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>
|
<template>
|
||||||
<!-- Auto-hidden chrome that ALSO looked like real content — surfaced PROACTIVELY
|
<!-- System-tag auto-applies (chrome hides / process WIP tags) that ALSO looked
|
||||||
atop the gallery whenever there's something to review (NOT gated on the
|
like real content — surfaced PROACTIVELY atop the gallery whenever there's
|
||||||
Show-hidden toggle, so misfires can't go unnoticed), most-concerning first,
|
something to review (NOT gated on the Show-hidden toggle, so misfires can't
|
||||||
with keep / un-hide (#141). Renders nothing when there's nothing to review. -->
|
go unnoticed), most-concerning first, with keep / remove (#141, #1464).
|
||||||
<section v-if="items.length" class="fc-review" aria-label="Hidden images to review">
|
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">
|
<div class="fc-review__head">
|
||||||
<v-icon size="18" color="warning">mdi-alert-outline</v-icon>
|
<v-icon size="18" color="warning">mdi-alert-outline</v-icon>
|
||||||
<span class="fc-review__title">
|
<span class="fc-review__title">
|
||||||
{{ items.length }} auto-hidden {{ items.length === 1 ? 'image' : 'images' }}
|
{{ items.length }} auto-tagged {{ items.length === 1 ? 'image' : 'images' }}
|
||||||
may be real content — review before they stay hidden
|
may be real content — review
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="fc-review__cards">
|
<div class="fc-review__cards">
|
||||||
@@ -26,16 +27,16 @@
|
|||||||
>
|
>
|
||||||
also looks like <strong>{{ it.conflict_name || 'content' }}</strong>
|
also looks like <strong>{{ it.conflict_name || 'content' }}</strong>
|
||||||
</div>
|
</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">
|
<div class="fc-review-card__acts">
|
||||||
<button
|
<button
|
||||||
type="button" class="fc-review-btn fc-review-btn--keep"
|
type="button" class="fc-review-btn fc-review-btn--keep"
|
||||||
:disabled="busy.includes(keyOf(it))" @click="resolve(it, 'keep')"
|
:disabled="busy.includes(keyOf(it))" @click="resolve(it, 'keep')"
|
||||||
>Keep hidden</button>
|
>{{ keepLabel(it) }}</button>
|
||||||
<button
|
<button
|
||||||
type="button" class="fc-review-btn fc-review-btn--unhide"
|
type="button" class="fc-review-btn fc-review-btn--unhide"
|
||||||
:disabled="busy.includes(keyOf(it))" @click="resolve(it, 'unhide')"
|
:disabled="busy.includes(keyOf(it))" @click="resolve(it, 'unhide')"
|
||||||
>Un-hide</button>
|
>{{ removeLabel(it) }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -54,6 +55,11 @@ const items = ref([])
|
|||||||
const busy = ref([])
|
const busy = ref([])
|
||||||
|
|
||||||
function keyOf(it) { return `${it.image_id}:${it.tag_id}` }
|
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() {
|
async function load() {
|
||||||
// Fetched unconditionally on mount — the strip prompts for pending misfires
|
// 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}`)
|
await api.post(`/api/gallery/hidden-review/${it.image_id}/${it.tag_id}/${action}`)
|
||||||
items.value = items.value.filter((x) => keyOf(x) !== k)
|
items.value = items.value.filter((x) => keyOf(x) !== k)
|
||||||
if (action === 'unhide') {
|
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) {
|
} catch (e) {
|
||||||
toast({
|
toast({
|
||||||
|
|||||||
@@ -171,13 +171,13 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p class="fc-muted text-body-2 mb-3">
|
<p class="fc-muted text-body-2 mb-3">
|
||||||
Auto-hide banners and editor screenshots from the gallery once a head has
|
Auto-hide <code>banner</code> chrome from the gallery once a head has
|
||||||
learned them (≥ {{ minPositives }} examples) and clears
|
learned it (≥ {{ minPositives }} examples) and clears
|
||||||
{{ Math.round((presentationThresholdInput || 0) * 100) }}% confidence.
|
{{ Math.round((presentationThresholdInput || 0) * 100) }}% confidence.
|
||||||
<code>wip</code> is never auto-hidden. If a hidden image also looks like
|
(<code>wip</code> and <code>editor screenshot</code> are handled by the
|
||||||
real content (≥ {{ Math.round((presentationConflictInput || 0) * 100) }}%
|
process auto-tagger below.) If a hidden image also looks like real content
|
||||||
on a content tag), it's flagged in the Hidden view instead of buried.
|
(≥ {{ Math.round((presentationConflictInput || 0) * 100) }}% on a content
|
||||||
Every auto-hide is reversible.
|
tag), it's flagged for review instead of buried. Every auto-hide is reversible.
|
||||||
</p>
|
</p>
|
||||||
<div class="d-flex mb-3" style="gap: 12px;">
|
<div class="d-flex mb-3" style="gap: 12px;">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
@@ -195,6 +195,43 @@
|
|||||||
</div>
|
</div>
|
||||||
</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 -->
|
<!-- Performance / tuning -->
|
||||||
<div v-if="metricsConcepts.length" class="mt-5">
|
<div v-if="metricsConcepts.length" class="mt-5">
|
||||||
<div class="fc-section-h mb-1">How auto-apply is landing</div>
|
<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 presentationEnabled = ref(true)
|
||||||
const presentationThresholdInput = ref(0.90)
|
const presentationThresholdInput = ref(0.90)
|
||||||
const presentationConflictInput = ref(0.50)
|
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 autoRunning = computed(() => autoStatus.value?.running_id != null)
|
||||||
const lastSweep = computed(() =>
|
const lastSweep = computed(() =>
|
||||||
@@ -292,6 +332,9 @@ onMounted(async () => {
|
|||||||
presentationEnabled.value = s.presentation_auto_apply_enabled ?? true
|
presentationEnabled.value = s.presentation_auto_apply_enabled ?? true
|
||||||
presentationThresholdInput.value = s.presentation_auto_apply_threshold ?? 0.90
|
presentationThresholdInput.value = s.presentation_auto_apply_threshold ?? 0.90
|
||||||
presentationConflictInput.value = s.presentation_conflict_threshold ?? 0.50
|
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 */ }
|
} catch { /* non-fatal */ }
|
||||||
await refresh()
|
await refresh()
|
||||||
if (running.value) startPoll()
|
if (running.value) startPoll()
|
||||||
@@ -402,6 +445,32 @@ async function onSavePresentation() {
|
|||||||
settingBusy.value = false
|
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 onPreview() { startSweep(true) }
|
||||||
function onApplyNow() { startSweep(false) }
|
function onApplyNow() { startSweep(false) }
|
||||||
async function startSweep(dryRun) {
|
async function startSweep(dryRun) {
|
||||||
|
|||||||
Reference in New Issue
Block a user