feat(system-tags): process auto-tag settings UI + mode-aware review strip (#1464)
Frontend for the system-tag refactor (milestone #157 step 6). - HeadsCard: new 'Auto-tag work-in-progress' section (enable + tag-confidence + conflict knobs) for wip/editor process auto-apply, mirroring the chrome card; copy notes they stay VISIBLE and the head only learns from titles/manual (no runaway). Presentation copy narrowed to banner-only. - HiddenReviewStrip: mode-aware — chrome flags read 'hidden as X / Keep hidden / Un-hide'; process flags read 'auto-tagged X / Keep tag / Remove tag'. Same endpoints (the backend returns mode), different words. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,14 +1,15 @@
|
||||
<template>
|
||||
<!-- Auto-hidden chrome that ALSO looked like real content — surfaced PROACTIVELY
|
||||
atop the gallery whenever there's something to review (NOT gated on the
|
||||
Show-hidden toggle, so misfires can't go unnoticed), most-concerning first,
|
||||
with keep / un-hide (#141). Renders nothing when there's nothing to review. -->
|
||||
<section v-if="items.length" class="fc-review" aria-label="Hidden images to review">
|
||||
<!-- System-tag auto-applies (chrome hides / process WIP tags) that ALSO looked
|
||||
like real content — surfaced PROACTIVELY atop the gallery whenever there's
|
||||
something to review (NOT gated on the Show-hidden toggle, so misfires can't
|
||||
go unnoticed), most-concerning first, with keep / remove (#141, #1464).
|
||||
Renders nothing when there's nothing to review. -->
|
||||
<section v-if="items.length" class="fc-review" aria-label="Auto-tagged images to review">
|
||||
<div class="fc-review__head">
|
||||
<v-icon size="18" color="warning">mdi-alert-outline</v-icon>
|
||||
<span class="fc-review__title">
|
||||
{{ items.length }} auto-hidden {{ items.length === 1 ? 'image' : 'images' }}
|
||||
may be real content — review before they stay hidden
|
||||
{{ items.length }} auto-tagged {{ items.length === 1 ? 'image' : 'images' }}
|
||||
may be real content — review
|
||||
</span>
|
||||
</div>
|
||||
<div class="fc-review__cards">
|
||||
@@ -26,16 +27,16 @@
|
||||
>
|
||||
also looks like <strong>{{ it.conflict_name || 'content' }}</strong>
|
||||
</div>
|
||||
<div class="fc-review-card__tag">hidden as {{ it.tag_name }}</div>
|
||||
<div class="fc-review-card__tag">{{ tagLine(it) }}</div>
|
||||
<div class="fc-review-card__acts">
|
||||
<button
|
||||
type="button" class="fc-review-btn fc-review-btn--keep"
|
||||
:disabled="busy.includes(keyOf(it))" @click="resolve(it, 'keep')"
|
||||
>Keep hidden</button>
|
||||
>{{ keepLabel(it) }}</button>
|
||||
<button
|
||||
type="button" class="fc-review-btn fc-review-btn--unhide"
|
||||
:disabled="busy.includes(keyOf(it))" @click="resolve(it, 'unhide')"
|
||||
>Un-hide</button>
|
||||
>{{ removeLabel(it) }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -54,6 +55,11 @@ const items = ref([])
|
||||
const busy = ref([])
|
||||
|
||||
function keyOf(it) { return `${it.image_id}:${it.tag_id}` }
|
||||
// Chrome flags hide the image (keep-hidden / un-hide); process flags leave it
|
||||
// visible and just tagged (keep-tag / remove-tag). Same endpoints, different words.
|
||||
function tagLine(it) { return (it.mode === 'process' ? 'auto-tagged ' : 'hidden as ') + it.tag_name }
|
||||
function keepLabel(it) { return it.mode === 'process' ? 'Keep tag' : 'Keep hidden' }
|
||||
function removeLabel(it) { return it.mode === 'process' ? 'Remove tag' : 'Un-hide' }
|
||||
|
||||
async function load() {
|
||||
// Fetched unconditionally on mount — the strip prompts for pending misfires
|
||||
@@ -71,7 +77,8 @@ async function resolve(it, action) {
|
||||
await api.post(`/api/gallery/hidden-review/${it.image_id}/${it.tag_id}/${action}`)
|
||||
items.value = items.value.filter((x) => keyOf(x) !== k)
|
||||
if (action === 'unhide') {
|
||||
toast({ text: `Un-hidden — “${it.tag_name}” removed; it'll train the head`, type: 'success' })
|
||||
const verb = it.mode === 'process' ? 'Removed' : 'Un-hidden'
|
||||
toast({ text: `${verb} — “${it.tag_name}” removed; it'll train the head`, type: 'success' })
|
||||
}
|
||||
} catch (e) {
|
||||
toast({
|
||||
|
||||
@@ -171,13 +171,13 @@
|
||||
/>
|
||||
</div>
|
||||
<p class="fc-muted text-body-2 mb-3">
|
||||
Auto-hide banners and editor screenshots from the gallery once a head has
|
||||
learned them (≥ {{ minPositives }} examples) and clears
|
||||
Auto-hide <code>banner</code> chrome from the gallery once a head has
|
||||
learned it (≥ {{ minPositives }} examples) and clears
|
||||
{{ Math.round((presentationThresholdInput || 0) * 100) }}% confidence.
|
||||
<code>wip</code> is never auto-hidden. If a hidden image also looks like
|
||||
real content (≥ {{ Math.round((presentationConflictInput || 0) * 100) }}%
|
||||
on a content tag), it's flagged in the Hidden view instead of buried.
|
||||
Every auto-hide is reversible.
|
||||
(<code>wip</code> and <code>editor screenshot</code> are handled by the
|
||||
process auto-tagger below.) If a hidden image also looks like real content
|
||||
(≥ {{ Math.round((presentationConflictInput || 0) * 100) }}% on a content
|
||||
tag), it's flagged for review instead of buried. Every auto-hide is reversible.
|
||||
</p>
|
||||
<div class="d-flex mb-3" style="gap: 12px;">
|
||||
<v-text-field
|
||||
@@ -195,6 +195,43 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Process auto-tagging (#1464): wip / editor screenshot -->
|
||||
<div class="fc-auto mt-6">
|
||||
<div class="d-flex align-center mb-1" style="gap: 10px;">
|
||||
<v-icon size="18" color="accent">mdi-progress-wrench</v-icon>
|
||||
<span class="fc-section-h">Auto-tag work-in-progress</span>
|
||||
<v-switch
|
||||
v-model="processEnabled" :loading="settingBusy" hide-details
|
||||
density="compact" color="success" class="ml-auto"
|
||||
@update:model-value="onToggleProcess"
|
||||
/>
|
||||
</div>
|
||||
<p class="fc-muted text-body-2 mb-3">
|
||||
Auto-tag <code>wip</code> and <code>editor screenshot</code> process art
|
||||
once a head has learned them (≥ {{ minPositives }} examples) and clears
|
||||
{{ Math.round((processThresholdInput || 0) * 100) }}% confidence. These stay
|
||||
<strong>visible</strong> in the gallery — the tag just keeps them out of
|
||||
training and the Explore rabbit-hole. Off by default. If a tagged image also
|
||||
looks like real content (≥ {{ Math.round((processConflictInput || 0) * 100) }}%
|
||||
on a content tag), it's flagged for review. Learns only from your titles +
|
||||
manual tags, never its own guesses — so it can't run away. Every tag reversible.
|
||||
</p>
|
||||
<div class="d-flex mb-3" style="gap: 12px;">
|
||||
<v-text-field
|
||||
v-model.number="processThresholdInput" label="Tag confidence"
|
||||
type="number" min="0.5" max="0.999" step="0.01" density="compact"
|
||||
hide-details style="max-width: 200px;" :disabled="settingBusy"
|
||||
@change="onSaveProcess"
|
||||
/>
|
||||
<v-text-field
|
||||
v-model.number="processConflictInput" label="Flag if content ≥"
|
||||
type="number" min="0" max="1" step="0.05" density="compact"
|
||||
hide-details style="max-width: 200px;" :disabled="settingBusy"
|
||||
@change="onSaveProcess"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Performance / tuning -->
|
||||
<div v-if="metricsConcepts.length" class="mt-5">
|
||||
<div class="fc-section-h mb-1">How auto-apply is landing</div>
|
||||
@@ -258,6 +295,9 @@ let autoTimer = null
|
||||
const presentationEnabled = ref(true)
|
||||
const presentationThresholdInput = ref(0.90)
|
||||
const presentationConflictInput = ref(0.50)
|
||||
const processEnabled = ref(false)
|
||||
const processThresholdInput = ref(0.90)
|
||||
const processConflictInput = ref(0.50)
|
||||
|
||||
const autoRunning = computed(() => autoStatus.value?.running_id != null)
|
||||
const lastSweep = computed(() =>
|
||||
@@ -292,6 +332,9 @@ onMounted(async () => {
|
||||
presentationEnabled.value = s.presentation_auto_apply_enabled ?? true
|
||||
presentationThresholdInput.value = s.presentation_auto_apply_threshold ?? 0.90
|
||||
presentationConflictInput.value = s.presentation_conflict_threshold ?? 0.50
|
||||
processEnabled.value = s.process_auto_apply_enabled ?? false
|
||||
processThresholdInput.value = s.process_auto_apply_threshold ?? 0.90
|
||||
processConflictInput.value = s.process_conflict_threshold ?? 0.50
|
||||
} catch { /* non-fatal */ }
|
||||
await refresh()
|
||||
if (running.value) startPoll()
|
||||
@@ -402,6 +445,32 @@ async function onSavePresentation() {
|
||||
settingBusy.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function onToggleProcess(val) {
|
||||
settingBusy.value = true
|
||||
try {
|
||||
await mlSettings.patchSettings({ process_auto_apply_enabled: !!val })
|
||||
toast({ text: val ? 'WIP auto-tag on' : 'WIP auto-tag off', type: 'success' })
|
||||
} catch (e) {
|
||||
processEnabled.value = !val // revert the switch
|
||||
toast({ text: `Could not update: ${e.message}`, type: 'error' })
|
||||
} finally {
|
||||
settingBusy.value = false
|
||||
}
|
||||
}
|
||||
async function onSaveProcess() {
|
||||
settingBusy.value = true
|
||||
try {
|
||||
await mlSettings.patchSettings({
|
||||
process_auto_apply_threshold: Number(processThresholdInput.value),
|
||||
process_conflict_threshold: Number(processConflictInput.value),
|
||||
})
|
||||
} catch (e) {
|
||||
toast({ text: `Could not save: ${e.message}`, type: 'error' })
|
||||
} finally {
|
||||
settingBusy.value = false
|
||||
}
|
||||
}
|
||||
function onPreview() { startSweep(true) }
|
||||
function onApplyNow() { startSweep(false) }
|
||||
async function startSweep(dryRun) {
|
||||
|
||||
Reference in New Issue
Block a user