@@ -1,303 +0,0 @@
< template >
< MaintenanceTile
icon = "mdi-flask-outline"
title = "Tagging eval (heads vs centroid)"
blurb = "Measure whether a trained head beats the old centroid on your own tags — and whether tagging more sharpens it."
:open = "running"
>
< p class = "fc-muted text-body-2 mb-3" >
Reuses the SigLIP embeddings already stored on your images ( no re - embed , no
GPU ) . For each concept it trains a logistic - regression < strong > head < / strong >
on your positives + negatives and compares it to the old single
< strong > centroid < / strong > , with cross - validated AP / F1 and a learning curve .
Runs as a background task ; the result is saved and reloads here .
< / p >
< v-textarea
v-model = "conceptsText" label="Concepts (comma-separated)"
rows = "2" auto -grow density = "compact" hide -details class = "mb-3"
:disabled = "running"
/ >
< div class = "d-flex mb-3" style = "gap: 12px;" >
< v-text-field
v -model .number = " autoTopN " label = "+ auto-add top-N concepts"
type = "number" min = "0" max = "200" density = "compact" hide -details
:disabled = "running" style = "max-width: 220px;"
/ >
< v-text-field
v -model .number = " precisionTarget " label = "Auto-apply precision target"
type = "number" min = "0.5" max = "0.999" step = "0.01" density = "compact" hide -details
:disabled = "running" style = "max-width: 220px;"
/ >
< / div >
< v-btn
v-if = "!running"
color = "accent" variant = "flat" rounded = "pill"
prepend -icon = " mdi -play " :loading = "busy" @click ="onStart"
> Run eval < / v-btn >
< div v-if = "running" class="mt-3" >
< v -progress -linear indeterminate color = "accent" / >
< div class = "text-body-2 mt-2 fc-muted" > Running … ( started { { startedAgo } } ) < / div >
< / div >
< v-alert
v-if = "run && run.status === 'error'"
type = "error" variant = "tonal" density = "compact" class = "mt-3"
> Eval failed : { { run . error } } < / v-alert >
< div v-if = "report" class="mt-4" >
< div class = "fc-muted text-caption mb-2" >
Ran { { formatTime ( report . generated _at ) } } ·
{ { report . concepts . length } } concept ( s ) ·
neg ratio { { report . params . neg _ratio } } , { { report . params . cv _folds } } - fold CV
< / div >
< div v-for = "c in report.concepts" :key="c.name" class="fc-cc" >
< div class = "fc-cc__head" >
< span class = "fc-cc__name" > { { c . name } } < / span >
< span v-if = "c.skipped" class="fc-muted text-caption" > — skipped : {{ c.skipped }} < / span >
< span v-else class = "fc-muted text-caption" >
{ { c . n _pos } } pos · { { c . n _neg } } neg < span v-if = "c.n_rejected" > ( {{ c.n_rejected }} rejected ) < / span >
< / span >
< / div >
< template v-if = "!c.skipped" >
< table class = "fc-metrics" >
< thead >
< tr > < th > < / th > < th > AP < / th > < th > F1 < / th > < th > Prec < / th > < th > Rec < / th > < / tr >
< / thead >
< tbody >
< tr >
< td class = "fc-metrics__lbl" > Head < / td >
< td class = "fc-num fc-win" > { { c . head . ap } } < / td >
< td class = "fc-num" > { { c . head . f1 } } < / td >
< td class = "fc-num" > { { c . head . precision } } < / td >
< td class = "fc-num" > { { c . head . recall } } < / td >
< / tr >
< tr >
< td class = "fc-metrics__lbl fc-muted" > Centroid < / td >
< td class = "fc-num fc-muted" > { { c . centroid . ap } } < / td >
< td class = "fc-num fc-muted" > { { c . centroid . f1 } } < / td >
< td class = "fc-num fc-muted" > { { c . centroid . precision } } < / td >
< td class = "fc-num fc-muted" > { { c . centroid . recall } } < / td >
< / tr >
< / tbody >
< / table >
< div class = "text-caption mb-2" : class = "apDelta(c) >= 0 ? 'fc-up' : 'fc-down'" >
Δ AP { { apDelta ( c ) >= 0 ? '+' : '' } } { { apDelta ( c ) . toFixed ( 3 ) } }
( head − centroid )
< / div >
< div class = "text-caption mb-2" >
< span class = "fc-muted" > Auto - apply : < / span >
< template v-if = "c.head.auto_apply" >
< span class = "fc-up" > ready < / span > — at P ≥ { { c . head . auto _apply . target } }
catches recall < strong > { { c . head . auto _apply . recall } } < / strong >
( thr { { c . head . auto _apply . threshold } } )
< / template >
< span v-else class = "fc-down" > not reachable at P ≥ { { report . params . precision _target } } < / span >
< / div >
< div v-if = "c.curve && c.curve.length" class="fc-curve" >
< span class = "fc-muted text-caption" > Learning curve ( AP @ N positives ) : < / span >
< span v-for = "p in c.curve" :key="p.n_pos" class="fc-curve__pt" >
{{ p.n_pos }} → < strong > { { p . ap } } < / strong >
< / span >
< / div >
< div v-if = "c.examples" class="fc-ex" >
< div
v-for ="grp in [
{ dir: 'suggest', items: c.examples.head_would_suggest,
label: `Head would suggest — ✓ tag it, ✗ not ${c.name}` },
{ dir: 'doubts', items: c.examples.head_doubts_positive,
label: `Head doubts your tag — ✓ keep, ✗ remove (not ${c.name})` },
]" :key="grp.dir" class="fc-ex__row"
>
<div class="fc-muted text-caption mb-1">{{ grp.label }}</div>
<div class="fc-ex__thumbs">
<div
v-for="it in grp.items" :key="`${grp.dir}${it.id}`"
class="fc-ex__item"
:class="actedLabel(c, grp.dir, it) ? 'fc-ex__item--acted' : ''"
>
<button
type="button" class="fc-ex__thumb"
:title="`#${it.id} — click to enlarge`" @click="modal.open(it.id)"
>
<img :src="it.thumbnail_url" loading="lazy" />
</button>
<div v-if="actedLabel(c, grp.dir, it)" class="fc-ex__badge">
{{ actedLabel(c, grp.dir, it) }}
</div>
<div v-else class="fc-ex__acts">
<button
class="fc-act fc-act--yes" type="button"
:title="`Yes — it is ${c.name}`" @click="act(c, it, grp.dir, 'yes')"
><v-icon size="15">mdi-check</v-icon></button>
<button
class="fc-act fc-act--no" type="button"
:title="`No — not ${c.name}`" @click="act(c, it, grp.dir, 'no')"
><v-icon size="15" > mdi -close < / v-icon > < / button >
< / div >
< / div >
< / div >
< / div >
< / div >
< / template >
< / div >
< / div >
< / MaintenanceTile >
< / template >
< script setup >
import { toast } from '../../utils/toast.js'
import { computed , onMounted , onUnmounted , ref } from 'vue'
import MaintenanceTile from '../common/MaintenanceTile.vue'
import { useTagEvalStore } from '../../stores/tagEval.js'
import { useModalStore } from '../../stores/modal.js'
const DEFAULT _CONCEPTS =
'glasses, cat, dog, horse, goblin, cum, lactation, fellatio, xray, stomach bulge'
const store = useTagEvalStore ( )
const modal = useModalStore ( )
const run = ref ( null )
const conceptsText = ref ( DEFAULT _CONCEPTS )
const autoTopN = ref ( 0 )
const precisionTarget = ref ( 0.97 )
const busy = ref ( false )
let pollTimer = null
const running = computed ( ( ) => run . value ? . status === 'running' )
const report = computed ( ( ) => ( run . value ? . status === 'ready' ? run . value . report : null ) )
const startedAgo = computed ( ( ) =>
run . value ? . started _at ? formatTime ( run . value . started _at ) : '' )
// Rehydrate the persisted run on mount so the report survives navigation — the
// task runs backend-side regardless; we just reconnect to its row.
onMounted ( async ( ) => {
try {
const latest = await store . latest ( )
if ( latest ) {
run . value = await store . getRun ( latest . id )
if ( run . value . status === 'running' ) startPoll ( latest . id )
}
} catch { /* non-fatal — card still works for a fresh run */ }
} )
onUnmounted ( stopPoll )
function startPoll ( id ) {
stopPoll ( )
pollTimer = setInterval ( async ( ) => {
try {
run . value = await store . getRun ( id )
if ( run . value . status !== 'running' ) stopPoll ( )
} catch ( e ) {
stopPoll ( )
toast ( { text : ` Eval poll failed: ${ e . message } ` , type : 'error' } )
}
} , 5000 )
}
function stopPoll ( ) {
if ( pollTimer ) { clearInterval ( pollTimer ) ; pollTimer = null }
}
async function onStart ( ) {
busy . value = true
try {
const concepts = conceptsText . value . split ( ',' ) . map ( s => s . trim ( ) ) . filter ( Boolean )
const res = await store . start ( {
concepts ,
auto _top _n : Number ( autoTopN . value ) || 0 ,
precision _target : Number ( precisionTarget . value ) || 0.97 ,
} )
run . value = await store . getRun ( res . run _id )
startPoll ( res . run _id )
} catch ( e ) {
const msg = e . body ? . running _id
? 'An eval is already running.'
: e . message
toast ( { text : ` Could not start eval: ${ msg } ` , type : 'error' } )
} finally {
busy . value = false
}
}
function apDelta ( c ) { return ( c . head ? . ap ? ? 0 ) - ( c . centroid ? . ap ? ? 0 ) }
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 { call = store . confirmTag ( it . id , c . tag _id ) ; label = 'kept' } // doubt + yes = keep (confirm)
try {
await call
acted . value [ key ] = label
} catch ( e ) {
toast ( { text : ` Action failed: ${ e . message } ` , type : 'error' } )
}
}
< / script >
< style scoped >
. fc - muted { color : rgb ( var ( -- v - theme - on - surface - variant ) ) ; }
. fc - cc {
padding : 12 px 0 ;
border - top : 1 px solid rgb ( var ( -- v - theme - surface - light ) ) ;
}
. fc - cc _ _head { display : flex ; align - items : baseline ; gap : 8 px ; margin - bottom : 6 px ; }
. fc - cc _ _name { font - weight : 600 ; }
. fc - metrics { width : 100 % ; max - width : 360 px ; border - collapse : collapse ; font - size : 13 px ; }
. fc - metrics th { text - align : right ; font - weight : 600 ; color : rgb ( var ( -- v - theme - on - surface - variant ) ) ; padding : 0 8 px ; }
. fc - metrics _ _lbl { text - align : left ; }
. fc - num { text - align : right ; font - variant - numeric : tabular - nums ; padding : 1 px 8 px ; }
. fc - win { color : rgb ( var ( -- v - theme - accent ) ) ; font - weight : 600 ; }
. fc - up { color : rgb ( var ( -- v - theme - success ) ) ; }
. fc - down { color : rgb ( var ( -- v - theme - error ) ) ; }
. fc - curve { margin - bottom : 8 px ; }
. fc - curve _ _pt { margin - left : 10 px ; font - size : 13 px ; font - variant - numeric : tabular - nums ; }
. fc - ex _ _row { margin - top : 8 px ; }
. fc - ex _ _thumbs { display : flex ; flex - wrap : wrap ; gap : 6 px ; }
. fc - ex _ _item { position : relative ; width : 120 px ; height : 120 px ; }
. fc - ex _ _item -- acted { opacity : 0.45 ; }
. fc - ex _ _thumb {
display : block ; width : 100 % ; height : 100 % ; border - radius : 6 px ;
overflow : hidden ; background : rgb ( var ( -- v - theme - surface - light ) ) ;
outline : 1 px solid transparent ; transition : outline - color 0.12 s ;
border : none ; padding : 0 ; cursor : pointer ;
}
. fc - ex _ _thumb : hover { outline - color : rgb ( var ( -- v - theme - accent ) ) ; }
. fc - ex _ _thumb img { width : 100 % ; height : 100 % ; object - fit : cover ; display : block ; }
. fc - ex _ _acts { position : absolute ; top : 4 px ; right : 4 px ; display : flex ; gap : 4 px ; }
. fc - act {
width : 26 px ; height : 26 px ; border - radius : 50 % ; border : none ; cursor : pointer ;
display : flex ; align - items : center ; justify - content : center ; color : # fff ;
opacity : 0.9 ; box - shadow : 0 1 px 3 px rgba ( 0 , 0 , 0 , 0.4 ) ; transition : transform 0.1 s ;
}
. fc - act : hover { opacity : 1 ; transform : scale ( 1.1 ) ; }
. fc - act -- yes { background : rgb ( var ( -- v - theme - success ) ) ; }
. fc - act -- no { background : rgb ( var ( -- v - theme - error ) ) ; }
. fc - ex _ _badge {
position : absolute ; bottom : 4 px ; left : 4 px ; right : 4 px ; text - align : center ;
font - size : 10 px ; text - transform : uppercase ; letter - spacing : 0.05 em ;
background : rgba ( 0 , 0 , 0 , 0.65 ) ; color : # fff ; border - radius : 3 px ; padding : 1 px 0 ;
}
< / style >