diff --git a/frontend/src/components/settings/TagEvalCard.vue b/frontend/src/components/settings/TagEvalCard.vue
index 70f3735..0a59b79 100644
--- a/frontend/src/components/settings/TagEvalCard.vue
+++ b/frontend/src/components/settings/TagEvalCard.vue
@@ -86,22 +86,41 @@
-
-
Head would suggest (untagged, high score)
+
+
{{ grp.label }}
-
![]()
-
-
-
-
Head doubts these (your positives, low score)
-
-
![]()
+
+
+
+
+
+ {{ actedLabel(c, grp.dir, it) }}
+
+
+
+
+
+
@@ -183,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 }
})