diff --git a/frontend/src/components/settings/TagEvalCard.vue b/frontend/src/components/settings/TagEvalCard.vue index ca218cc..0a59b79 100644 --- a/frontend/src/components/settings/TagEvalCard.vue +++ b/frontend/src/components/settings/TagEvalCard.vue @@ -86,28 +86,41 @@
@@ -189,6 +202,28 @@ function formatTime(iso) { if (!iso) return '' try { return new Date(iso).toLocaleString() } catch { return iso } } + +// Acting on an example writes the SAME tables the head trains on, so a re-run +// reflects the correction. Keyed per (concept, list, image); the report ids are +// frozen at run time, so we just grey out what's been handled in this view. +const acted = ref({}) +const actedKey = (c, dir, it) => `${c.tag_id}:${dir}:${it.id}` +const actedLabel = (c, dir, it) => acted.value[actedKey(c, dir, it)] || '' + +async function act(c, it, dir, verdict) { + const key = actedKey(c, dir, it) + let call, label + if (dir === 'suggest' && verdict === 'yes') { call = store.applyTag(it.id, c.tag_id); label = 'tagged' } + else if (dir === 'suggest' && verdict === 'no') { call = store.rejectTag(it.id, c.tag_id); label = 'rejected' } + else if (dir === 'doubts' && verdict === 'no') { call = store.removeTag(it.id, c.tag_id); label = 'removed' } + else { acted.value[key] = 'kept'; return } // doubt + yes = keep, no write + try { + await call + acted.value[key] = label + } catch (e) { + toast({ text: `Action failed: ${e.message}`, type: 'error' }) + } +} diff --git a/frontend/src/stores/tagEval.js b/frontend/src/stores/tagEval.js index 044707b..da676f1 100644 --- a/frontend/src/stores/tagEval.js +++ b/frontend/src/stores/tagEval.js @@ -23,5 +23,29 @@ export const useTagEvalStore = defineStore('tagEval', () => { return (body.runs && body.runs[0]) || null } - return { start, getRun, latest } + // --- Acting on the head's example lists (closes the learn-from-tags loop). + // These write the SAME tables the head trains on: image_tag (positives) and + // tag_suggestion_rejection (negatives, via the dismiss endpoint). + + // "Yes, it is this" — apply the tag (new positive). + async function applyTag(imageId, tagId) { + return await api.post(`/api/images/${imageId}/tags`, + { body: { tag_id: tagId, source: 'manual' } }) + } + + // "No, it's not" on an UNtagged suggestion — record a rejection (hard negative). + async function rejectTag(imageId, tagId) { + return await api.post(`/api/images/${imageId}/suggestions/dismiss`, + { body: { tag_id: tagId } }) + } + + // "Not it" on one of YOUR positives the head doubts — remove the tag AND + // record the rejection (kills the bad positive, leaves a hard negative). + async function removeTag(imageId, tagId) { + await api.delete(`/api/images/${imageId}/tags/${tagId}`) + return await api.post(`/api/images/${imageId}/suggestions/dismiss`, + { body: { tag_id: tagId } }) + } + + return { start, getRun, latest, applyTag, rejectTag, removeTag } })