mirror of
https://github.com/AstrBotDevs/AstrBot.git
synced 2026-09-24 16:39:52 +08:00
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:
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user