feat(fc2b): AliasPickerDialog + TagRenameDialog
AliasPicker: debounced autocomplete scoped to the prediction's category (artist/character map to tag kinds; copyright searches unscoped), returns the chosen canonical tag id. RenameDialog: PATCH /api/tags/<id>, surfaces 409 collisions with the FC-2c merge hint. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
<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. 'copyright' has no tag kind; search unscoped there.
|
||||
const kind = ['artist', 'character'].includes(props.category)
|
||||
? props.category : 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>
|
||||
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-card-title>Rename tag</v-card-title>
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
v-model="newName" label="New name" density="compact"
|
||||
autofocus @keydown.enter="submit"
|
||||
/>
|
||||
<v-alert v-if="errorMsg" type="error" variant="tonal" density="compact" class="mt-2">
|
||||
{{ errorMsg }}
|
||||
<div v-if="isCollision" class="text-caption mt-1">
|
||||
Merging two tags into one lands in FC-2c.
|
||||
</div>
|
||||
</v-alert>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn @click="$emit('cancel')">Cancel</v-btn>
|
||||
<v-btn
|
||||
color="primary" rounded="pill"
|
||||
:disabled="!newName.trim() || newName === tag.name"
|
||||
:loading="busy" @click="submit"
|
||||
>Rename</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useApi } from '../../composables/useApi.js'
|
||||
|
||||
const props = defineProps({ tag: { type: Object, required: true } })
|
||||
const emit = defineEmits(['renamed', 'cancel'])
|
||||
|
||||
const api = useApi()
|
||||
const newName = ref(props.tag.name)
|
||||
const errorMsg = ref(null)
|
||||
const isCollision = ref(false)
|
||||
const busy = ref(false)
|
||||
|
||||
async function submit() {
|
||||
if (!newName.value.trim() || newName.value === props.tag.name) return
|
||||
busy.value = true
|
||||
errorMsg.value = null
|
||||
isCollision.value = false
|
||||
try {
|
||||
const updated = await api.patch(`/api/tags/${props.tag.id}`, {
|
||||
body: { name: newName.value.trim() }
|
||||
})
|
||||
emit('renamed', updated)
|
||||
} catch (e) {
|
||||
errorMsg.value = e.message
|
||||
isCollision.value = e.status === 409
|
||||
} finally {
|
||||
busy.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user