Soft auto-apply (retract + confirm, no self-training) + tagging UX (reject-rest, tag-input race, modal playlist) #197

Merged
bvandeusen merged 12 commits from dev into main 2026-07-06 21:03:30 -04:00
Showing only changes of commit d3984ccb0d - Show all commits
@@ -97,6 +97,7 @@
import { computed, nextTick, onMounted, ref, watch } from 'vue'
import { useTagStore } from '../../stores/tags.js'
import { useSuggestionsStore } from '../../stores/suggestions.js'
import { useInflightToken } from '../../composables/useInflightToken.js'
import FandomPicker from './FandomPicker.vue'
const emit = defineEmits(['pick-existing', 'pick-new', 'accept-suggestion', 'cancel'])
@@ -183,17 +184,26 @@ const parsed = computed(() => {
const parsedKind = computed(() => parsed.value.kind)
const parsedName = computed(() => parsed.value.name)
// Inflight guard: the debounce only clears the TIMER, so once a fetch has fired
// it still races later ones — a slower earlier-prefix response ("s") could land
// after "sex" and overwrite the dropdown with stale, wrong-prefix matches
// (operator-flagged 2026-07-06). Gate each response on a token so only the latest
// query's results are applied.
const acInflight = useInflightToken()
let debounceId = null
watch(query, () => {
highlight.value = 0
if (debounceId) clearTimeout(debounceId)
acInflight.cancel()
debounceId = setTimeout(async () => {
const q = parsedName.value
if (!q) { hits.value = []; return }
// Autocomplete across ALL kinds. When the user typed a prefix the
// matches list is naturally narrower because the parsed name is
// shorter; we don't filter server-side by kind.
hits.value = await store.autocomplete(q, null, 10)
const t = acInflight.claim()
const res = await store.autocomplete(q, null, 10)
if (t.isCurrent()) hits.value = res
}, 200)
})