fix: prevent IME enter from sending chat (#7845)

* fix: prevent IME enter from sending chat

* fix: prevent IME enter from sending chat

* refactor: clarify IME composition state handling
This commit is contained in:
エイカク
2026-04-27 22:56:24 +09:00
committed by GitHub
parent 72d65680b8
commit 67c7445d25
3 changed files with 96 additions and 0 deletions
@@ -112,6 +112,10 @@
ref="inputField"
v-model="localPrompt"
@keydown="handleKeyDown"
@compositionstart="handleCompositionStart"
@compositionend="handleCompositionEnd"
@compositioncancel="handleCompositionEnd"
@blur="clearCompositionState()"
:disabled="disabled"
placeholder="Ask AstrBot..."
class="chat-textarea"
@@ -307,6 +311,7 @@ import {
import { useDisplay } from "vuetify";
import { useModuleI18n } from "@/i18n/composables";
import { useCustomizerStore } from "@/stores/customizer";
import { isComposingEnter } from "@/utils/imeInput.mjs";
import ConfigSelector from "./ConfigSelector.vue";
import ProviderModelMenu from "./ProviderModelMenu.vue";
import StyledMenu from "@/components/shared/StyledMenu.vue";
@@ -379,6 +384,8 @@ const providerModelMenuRef = ref<InstanceType<typeof ProviderModelMenu> | null>(
const showProviderSelector = ref(true);
const isReplyClosing = ref(false);
const isDragging = ref(false);
const isComposing = ref(false);
const lastCompositionEndAt = ref<number | null>(null);
let dragLeaveTimeout: number | null = null;
const localPrompt = computed({
@@ -514,6 +521,10 @@ function handleKeyDown(e: KeyboardEvent) {
return;
}
if (isComposingEnter(e, isComposing.value, lastCompositionEndAt.value)) {
return;
}
const isSendHotkey =
e.ctrlKey ||
e.metaKey ||
@@ -533,6 +544,23 @@ function handleKeyDown(e: KeyboardEvent) {
}
}
function handleCompositionStart() {
isComposing.value = true;
lastCompositionEndAt.value = null;
}
function handleCompositionEnd(e: CompositionEvent) {
lastCompositionEndAt.value = e.timeStamp;
clearCompositionState({ keepLastEndAt: true });
}
function clearCompositionState({ keepLastEndAt = false } = {}) {
isComposing.value = false;
if (!keepLastEndAt) {
lastCompositionEndAt.value = null;
}
}
function handleKeyUp(e: KeyboardEvent) {
if (e.keyCode === 66) {
ctrlKeyDown.value = false;
@@ -634,6 +662,7 @@ onBeforeUnmount(() => {
if (inputField.value) {
inputField.value.removeEventListener("paste", handlePaste);
}
clearCompositionState();
document.removeEventListener("keyup", handleKeyUp);
});
+31
View File
@@ -0,0 +1,31 @@
// Some IMEs emit Enter right after compositionend; treat that same-keystroke
// window as composition so selecting a candidate does not send the message.
const RECENT_COMPOSITION_END_THRESHOLD_MS = 100;
/**
* @param {KeyboardEvent} event
* @param {boolean} compositionActive
* @param {number | null} lastCompositionEndAt
*/
export function isComposingEnter(
event,
compositionActive,
lastCompositionEndAt = null,
) {
const hasLegacyCompositionKeyCode =
typeof event.keyCode === "number" && event.keyCode === 229;
const isAfterRecentCompositionEnd =
typeof event.timeStamp === "number" &&
typeof lastCompositionEndAt === "number" &&
event.timeStamp >= lastCompositionEndAt &&
event.timeStamp - lastCompositionEndAt <
RECENT_COMPOSITION_END_THRESHOLD_MS;
return (
event.key === "Enter" &&
(compositionActive ||
event.isComposing ||
hasLegacyCompositionKeyCode ||
isAfterRecentCompositionEnd)
);
}
+36
View File
@@ -0,0 +1,36 @@
import assert from "node:assert/strict";
import test from "node:test";
import { isComposingEnter } from "../src/utils/imeInput.mjs";
test("detects Enter while an IME composition is active", () => {
assert.equal(isComposingEnter({ key: "Enter", isComposing: true }, false), true);
assert.equal(isComposingEnter({ key: "Enter", isComposing: false }, true), true);
});
test("does not treat normal Enter as IME composition", () => {
assert.equal(isComposingEnter({ key: "Enter", isComposing: false }, false), false);
assert.equal(isComposingEnter({ key: "a", isComposing: true }, true), false);
});
test("detects Enter fired immediately after composition ended", () => {
assert.equal(
isComposingEnter(
{ key: "Enter", isComposing: false, timeStamp: 105 },
false,
100,
),
true,
);
});
test("does not treat delayed Enter after composition ended as IME composition", () => {
assert.equal(
isComposingEnter(
{ key: "Enter", isComposing: false, timeStamp: 250 },
false,
100,
),
false,
);
});