Files
FabledCurator/frontend/src/composables/useHeadTraining.js
T
bvandeusen 77baee49fd
CI / lint (push) Successful in 3s
CI / frontend-build (push) Successful in 19s
CI / backend-lint-and-test (push) Successful in 26s
CI / integration (push) Successful in 3m26s
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
2026-06-28 22:15:27 -04:00

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 }
}