Four coupled operator-asked changes to the view modal (Scribe plan #509): 1. **Autofocus tag entry on modal open** — TagAutocomplete grabs focus in onMounted/nextTick so the caret is in the input the moment the modal renders. No click needed to start typing. 2. **General suggestions expanded by default** — SuggestionsPanel's general-category group now mounts with `:default-open="true"`. Operator can collapse if too noisy, but the v1 frame shows them. 3. **Lower general threshold default 0.95 → 0.50** — MLSettings. suggestion_threshold_general default matches character. Alembic 0029 also bumps the existing singleton row's value if it's still at the old 0.95. Operator can re-tune from Settings → ML. 4. **Retire `copyright` + `artist` as ML suggestion categories** — neither feeds a Tag.kind (`artist` retired in FC-2d-vii-c, never really existed as a copyright tag-kind). They were surfaced in the suggestions pipeline + threshold settings UI but had no follow- through. Drop from SURFACED_CATEGORIES, suggestions._threshold_for, ml_admin GET/PATCH allowlist, MLSettings columns (alembic 0029 drops the two columns), frontend CATEGORY_ORDER + CATEGORY_LABELS, SuggestionsPanel.peopleCats, AliasPickerDialog kind-check, and MLThresholdSliders rows. Out of scope (intentional): `tag_kind` Postgres enum still includes `artist` for historic Tag row queryability (per the model comment); no operator pain reported, no enum-shrink needed. Tests: - test_surfaced_categories asserts {character, general}, excludes artist + copyright. - test_threshold_for_artist_is_unsurfaced extended to cover copyright. - test_get_and_patch_settings asserts new 0.50 default and the absent artist + copyright keys in the GET payload.
65 lines
2.0 KiB
Vue
65 lines
2.0 KiB
Vue
<template>
|
|
<v-card>
|
|
<v-card-title>Treat as alias for…</v-card-title>
|
|
<v-card-text>
|
|
<p class="text-caption mb-2">
|
|
Pick the existing tag the model's prediction should map to. All
|
|
future predictions of this name will resolve to your tag.
|
|
</p>
|
|
<v-autocomplete
|
|
v-model="selectedId"
|
|
:items="results"
|
|
:item-title="(t) => t.fandom_name ? `${t.name} — ${t.fandom_name}` : t.name"
|
|
:item-value="(t) => t.id"
|
|
:loading="loading"
|
|
label="Canonical tag"
|
|
no-filter clearable density="compact"
|
|
@update:search="onSearch"
|
|
/>
|
|
</v-card-text>
|
|
<v-card-actions>
|
|
<v-spacer />
|
|
<v-btn @click="$emit('cancel')">Cancel</v-btn>
|
|
<v-btn
|
|
color="primary" rounded="pill" :disabled="!selectedId"
|
|
@click="$emit('confirm', selectedId)"
|
|
>Use this tag</v-btn>
|
|
</v-card-actions>
|
|
</v-card>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref } from 'vue'
|
|
import { useApi } from '../../composables/useApi.js'
|
|
|
|
const props = defineProps({ category: { type: String, required: true } })
|
|
defineEmits(['confirm', 'cancel'])
|
|
|
|
const api = useApi()
|
|
const results = ref([])
|
|
const loading = ref(false)
|
|
const selectedId = ref(null)
|
|
let debounce = null
|
|
|
|
function onSearch(q) {
|
|
if (debounce) clearTimeout(debounce)
|
|
debounce = setTimeout(async () => {
|
|
const query = (q || '').trim()
|
|
if (!query) { results.value = []; return }
|
|
loading.value = true
|
|
try {
|
|
// Scope the autocomplete to the prediction's category where it
|
|
// maps to a tag kind. Only 'character' surfaces as both a
|
|
// suggestion category and a tag kind now ('artist' + 'copyright'
|
|
// retired); other categories search unscoped.
|
|
const kind = props.category === 'character' ? 'character' : null
|
|
const params = { q: query, limit: 20 }
|
|
if (kind) params.kind = kind
|
|
results.value = await api.get('/api/tags/autocomplete', { params })
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}, 200)
|
|
}
|
|
</script>
|