77baee49fd
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
68 lines
2.1 KiB
JavaScript
68 lines
2.1 KiB
JavaScript
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 }
|
|
}
|