From 4974b7cf77c9d8d0c453985507efa5aee0110a56 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Sat, 27 Jun 2026 23:19:58 -0400 Subject: [PATCH 1/2] feat(tag-eval): bigger, clickable example thumbnails (label-review queue) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 56px example thumbs were too small to judge a label (operator-flagged). Bump to 120px and wrap each in a link to /explore/ (new tab) so the "head doubts / would suggest" galleries double as a review-and-fix queue — click a doubted positive, land on it in Explore, correct the tag, re-run. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/components/settings/TagEvalCard.vue | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/frontend/src/components/settings/TagEvalCard.vue b/frontend/src/components/settings/TagEvalCard.vue index 70f3735..ca218cc 100644 --- a/frontend/src/components/settings/TagEvalCard.vue +++ b/frontend/src/components/settings/TagEvalCard.vue @@ -87,21 +87,27 @@
-
Head would suggest (untagged, high score)
+
Head would suggest (untagged, high score) — click to open in Explore
- + class="fc-ex__thumb" :href="`/explore/${it.id}`" target="_blank" + rel="noopener" :title="`#${it.id} — open in Explore`" + > + +
-
Head doubts these (your positives, low score)
+
Head doubts these (your positives, low score) — click to open in Explore
- + class="fc-ex__thumb" :href="`/explore/${it.id}`" target="_blank" + rel="noopener" :title="`#${it.id} — open in Explore`" + > + +
@@ -202,10 +208,13 @@ function formatTime(iso) { .fc-down { color: rgb(var(--v-theme-error)); } .fc-curve { margin-bottom: 8px; } .fc-curve__pt { margin-left: 10px; font-size: 13px; font-variant-numeric: tabular-nums; } -.fc-ex__row { margin-top: 6px; } -.fc-ex__thumbs { display: flex; flex-wrap: wrap; gap: 4px; } +.fc-ex__row { margin-top: 8px; } +.fc-ex__thumbs { display: flex; flex-wrap: wrap; gap: 6px; } .fc-ex__thumb { - width: 56px; height: 56px; object-fit: cover; border-radius: 4px; - background: rgb(var(--v-theme-surface-light)); + display: block; width: 120px; height: 120px; border-radius: 6px; + overflow: hidden; background: rgb(var(--v-theme-surface-light)); + outline: 1px solid transparent; transition: outline-color 0.12s; } +.fc-ex__thumb:hover { outline-color: rgb(var(--v-theme-accent)); } +.fc-ex__thumb img { width: 100%; height: 100%; object-fit: cover; display: block; } From 13d297b881f700ce5c226c04144bd292e0d66ffa Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Sat, 27 Jun 2026 23:37:21 -0400 Subject: [PATCH 2/2] feat(tag-eval): inline confirm/reject actions on example thumbnails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the learn-from-tags loop directly on the eval lists (operator-flagged: no surface to confirm/refine the head's suggestions). Each thumbnail gets a green ✓ / red ✗ that writes the SAME tables the head trains on: - suggest + ✓ → apply tag (new positive, POST /images//tags) - suggest + ✗ → record rejection (hard negative, suggestions/dismiss) - doubt + ✗ → remove tag + record rejection (kill bad positive, add negative) - doubt + ✓ → keep (stays a positive, no write) Acted thumbs grey out with a badge; re-run to see the head sharpen. Thumb still links to /explore/. All endpoints already existed — no backend change. Inline is the starting point; longer-term the modal Suggestions rail gets the red "No" (negative) so per-image rejection is native there too (next slice). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/components/settings/TagEvalCard.vue | 93 ++++++++++++++----- frontend/src/stores/tagEval.js | 26 +++++- 2 files changed, 97 insertions(+), 22 deletions(-) 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 @@
-
-
Head would suggest (untagged, high score) — click to open in Explore
+
+
{{ grp.label }}
- - - -
-
-
-
Head doubts these (your positives, low score) — click to open in Explore
-
- - - + + + +
+ {{ actedLabel(c, grp.dir, it) }} +
+
+ + +
+
@@ -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 } })