diff --git a/frontend/src/components/admin/channel/ModelTagInput.vue b/frontend/src/components/admin/channel/ModelTagInput.vue index a268ae340a..b8c7e8758e 100644 --- a/frontend/src/components/admin/channel/ModelTagInput.vue +++ b/frontend/src/components/admin/channel/ModelTagInput.vue @@ -23,9 +23,9 @@ type="text" class="flex-1 min-w-[120px] border-none bg-transparent text-sm outline-none placeholder:text-gray-400 dark:text-white" :placeholder="models.length === 0 ? placeholder : ''" - @keydown.enter.prevent="addModel" + @keydown.enter="handleEnter" @keydown.tab="handleTab" - @keydown.delete="handleBackspace" + @keydown.backspace="handleBackspace" @paste="handlePaste" @blur="addModel" /> @@ -66,8 +66,14 @@ function addModel() { inputValue.value = '' } +function handleEnter(event: KeyboardEvent) { + if (event.isComposing) return + event.preventDefault() + addModel() +} + function handleTab(event: KeyboardEvent) { - if (!inputValue.value.trim()) return + if (event.isComposing || !inputValue.value.trim()) return event.preventDefault() addModel() } @@ -78,7 +84,8 @@ function removeModel(idx: number) { emit('update:models', newModels) } -function handleBackspace() { +function handleBackspace(event: KeyboardEvent) { + if (event.isComposing) return if (inputValue.value === '' && props.models.length > 0) { removeModel(props.models.length - 1) } diff --git a/frontend/src/components/admin/channel/__tests__/ModelTagInput.keyboard.spec.ts b/frontend/src/components/admin/channel/__tests__/ModelTagInput.keyboard.spec.ts index 44e4cc67b7..ccf09d8880 100644 --- a/frontend/src/components/admin/channel/__tests__/ModelTagInput.keyboard.spec.ts +++ b/frontend/src/components/admin/channel/__tests__/ModelTagInput.keyboard.spec.ts @@ -13,6 +13,33 @@ function pressTab(input: Element, shiftKey = false) { } describe('model tag keyboard navigation', () => { + it.each(['Enter', 'Tab', 'Backspace'])('leaves composing %s to the input method', async (key) => { + const wrapper = mount(ModelTagInput, { props: { models: ['existing'] } }) + const input = wrapper.get('input') + if (key !== 'Backspace') await input.setValue('prefix') + const event = new KeyboardEvent('keydown', { key, isComposing: true, bubbles: true, cancelable: true }) + input.element.dispatchEvent(event) + expect(event.defaultPrevented).toBe(false) + expect(wrapper.emitted('update:models')).toBeUndefined() + }) + + it('keeps tags on Delete and removes the last tag only on Backspace', async () => { + const wrapper = mount(ModelTagInput, { props: { models: ['existing'] } }) + await wrapper.get('input').trigger('keydown', { key: 'Delete' }) + expect(wrapper.emitted('update:models')).toBeUndefined() + await wrapper.get('input').trigger('keydown', { key: 'Backspace' }) + expect(wrapper.emitted('update:models')).toEqual([[[]]]) + }) + + it('commits ordinary Enter input and prevents form submission', async () => { + const wrapper = mount(ModelTagInput, { props: { models: [] } }) + const input = wrapper.get('input'); await input.setValue('new-model') + const event = new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true }) + input.element.dispatchEvent(event) + expect(event.defaultPrevented).toBe(true) + expect(wrapper.emitted('update:models')).toEqual([[['new-model']]]) + }) + it.each([false, true])('allows leaving an empty input with Tab (shift: %s)', (shift) => { const wrapper = mount(ModelTagInput, { props: { models: ['gpt-4o'] } }) expect(pressTab(wrapper.get('input').element, shift).defaultPrevented).toBe(false)