feat(heads): nightly auto-retrain + inline Retrain button in Explore
Two cadences for keeping heads in sync with your tagging: - PASSIVE: a nightly `scheduled_train_heads` beat (skips if a run is already in flight; creates+commits the run row before dispatching train_heads so the ml worker always finds it). Folds the day's accepts/rejects + newly-eligible concepts into the heads without anyone clicking. - ACTIVE: a "Retrain heads" button in the Explore trail bar — bank the +/- feedback you just gave while walking content, without a trip to Settings. Shared logic in a new useHeadTraining composable (trigger + poll + start/finish toasts), used by the Explore button; reflects an already-running run (incl. the nightly one) on mount. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ttrj5P7upUTueSfoJcxEqa
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { useHeadsStore } from '../stores/heads.js'
|
||||
import { toast } from '../utils/toast.js'
|
||||
|
||||
// Shared "(re)train the concept heads" behaviour (#114): trigger + poll status,
|
||||
// with toasts on start/finish. Backs both the Settings card and the Explore
|
||||
// view's inline button so the active retrain works the same everywhere.
|
||||
// Call start() on mount and stop() on unmount to manage the poll timer.
|
||||
export function useHeadTraining() {
|
||||
const store = useHeadsStore()
|
||||
const summary = ref(null)
|
||||
const busy = ref(false) // the trigger POST is in flight
|
||||
let timer = null
|
||||
|
||||
const running = computed(() => summary.value?.running_id != null)
|
||||
const headCount = computed(() => summary.value?.head_count ?? 0)
|
||||
|
||||
async function refresh() {
|
||||
try {
|
||||
summary.value = await store.status()
|
||||
} catch { /* non-fatal — the button still offers a fresh train */ }
|
||||
}
|
||||
|
||||
function startPoll() {
|
||||
stopPoll()
|
||||
timer = setInterval(async () => {
|
||||
const wasRunning = running.value
|
||||
await refresh()
|
||||
// Announce completion once, on the running → done transition.
|
||||
if (wasRunning && !running.value) {
|
||||
toast({
|
||||
text: `Heads retrained — ${headCount.value} concept${headCount.value === 1 ? '' : 's'}`,
|
||||
type: 'success',
|
||||
})
|
||||
}
|
||||
if (!running.value) stopPoll()
|
||||
}, 5000)
|
||||
}
|
||||
|
||||
function stopPoll() {
|
||||
if (timer) { clearInterval(timer); timer = null }
|
||||
}
|
||||
|
||||
// Reflect an already-running run (e.g. the nightly one) on mount.
|
||||
async function start() {
|
||||
await refresh()
|
||||
if (running.value) startPoll()
|
||||
}
|
||||
|
||||
async function train() {
|
||||
busy.value = true
|
||||
try {
|
||||
await store.train()
|
||||
toast({ text: 'Head training started…', type: 'info' })
|
||||
await refresh()
|
||||
startPoll()
|
||||
} catch (e) {
|
||||
const msg = e.body?.running_id ? 'Training is already running.' : e.message
|
||||
toast({ text: `Could not start training: ${msg}`, type: 'error' })
|
||||
} finally {
|
||||
busy.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return { summary, running, busy, headCount, refresh, train, start, stop: stopPoll }
|
||||
}
|
||||
Reference in New Issue
Block a user