mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 13:16:24 +00:00
fix(frontend): ignore message edit shortcuts during IME composition (#6768)
Co-authored-by: LingYi-Liang <270316896+LingYi-Liang@users.noreply.github.com> Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
parent
e33024446a
commit
49adb0edec
@ -57,6 +57,7 @@ import {
|
||||
} from "@/core/skills";
|
||||
import { useSkills } from "@/core/skills/hooks";
|
||||
import { pathOfThread } from "@/core/threads/utils";
|
||||
import { isIMEComposing } from "@/lib/ime";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { WorkspaceChangeBadge } from "../changes";
|
||||
@ -589,6 +590,7 @@ function MessageContent_({
|
||||
editState.onDraftChange(event.currentTarget.value)
|
||||
}
|
||||
onKeyDown={(event) => {
|
||||
if (isIMEComposing(event)) return;
|
||||
if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
editState.onCancel();
|
||||
|
||||
69
frontend/tests/e2e/message-edit-ime.spec.ts
Normal file
69
frontend/tests/e2e/message-edit-ime.spec.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
import { MOCK_THREAD_ID, mockLangGraphAPI } from "./utils/mock-api";
|
||||
|
||||
for (const agentName of [undefined, "test-agent"]) {
|
||||
test(`message editing keeps composing shortcuts in ${agentName ?? "default"} chat`, async ({
|
||||
page,
|
||||
}) => {
|
||||
mockLangGraphAPI(page, {
|
||||
agents: agentName
|
||||
? [
|
||||
{
|
||||
name: agentName,
|
||||
description: "Test agent",
|
||||
system_prompt: "Test",
|
||||
},
|
||||
]
|
||||
: [],
|
||||
threads: [
|
||||
{
|
||||
thread_id: MOCK_THREAD_ID,
|
||||
title: "Editing with an IME",
|
||||
agent_name: agentName,
|
||||
messages: [
|
||||
{ type: "human", id: "human-1", content: "Original question" },
|
||||
{ type: "ai", id: "ai-1", content: "Original answer" },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
let rerunRequests = 0;
|
||||
await page.route("**/runs/edit-regenerate/prepare", async (route) => {
|
||||
rerunRequests++;
|
||||
await route.fulfill({
|
||||
status: 409,
|
||||
json: { detail: "Test rerun blocked" },
|
||||
});
|
||||
});
|
||||
await page.goto(
|
||||
agentName
|
||||
? `/workspace/agents/${agentName}/chats/${MOCK_THREAD_ID}`
|
||||
: `/workspace/chats/${MOCK_THREAD_ID}`,
|
||||
);
|
||||
await expect(
|
||||
page.getByText("Original answer", { exact: true }),
|
||||
).toBeVisible();
|
||||
await page.getByText("Original question", { exact: true }).hover();
|
||||
await page
|
||||
.getByRole("button", { name: "Edit and rerun", exact: true })
|
||||
.click();
|
||||
const editor = page.locator(".is-user textarea");
|
||||
await editor.fill("Unfinished draft");
|
||||
for (const event of [
|
||||
{ key: "Escape", isComposing: true },
|
||||
{ key: "Escape", keyCode: 229 },
|
||||
{ key: "Enter", ctrlKey: true, isComposing: true },
|
||||
{ key: "Enter", ctrlKey: true, keyCode: 229 },
|
||||
{ key: "Enter", metaKey: true, isComposing: true },
|
||||
{ key: "Enter", metaKey: true, keyCode: 229 },
|
||||
]) {
|
||||
await editor.dispatchEvent("keydown", event);
|
||||
await expect(editor).toHaveValue("Unfinished draft");
|
||||
expect(rerunRequests).toBe(0);
|
||||
}
|
||||
await editor.press("Escape");
|
||||
await expect(editor).toHaveCount(0);
|
||||
expect(rerunRequests).toBe(0);
|
||||
});
|
||||
}
|
||||
@ -71,6 +71,66 @@ afterEach(() => {
|
||||
copyDataCalls.mockClear();
|
||||
});
|
||||
|
||||
describe("MessageListItem edit shortcuts during IME composition", () => {
|
||||
function openEditor() {
|
||||
const onEditAndRegenerate = rs.fn(async () => true);
|
||||
const view = render(
|
||||
withI18n(
|
||||
<MessageListItem
|
||||
message={makeMessage("human")}
|
||||
threadId="thread-1"
|
||||
canEdit
|
||||
onEditAndRegenerate={onEditAndRegenerate}
|
||||
/>,
|
||||
),
|
||||
);
|
||||
fireEvent.click(
|
||||
view.getByRole("button", { name: enUS.common.editAndRerun }),
|
||||
);
|
||||
const editor = view.getByRole("textbox");
|
||||
fireEvent.change(editor, { target: { value: "unfinished draft" } });
|
||||
return { view, editor, onEditAndRegenerate };
|
||||
}
|
||||
|
||||
it.each([
|
||||
{ key: "Escape", isComposing: true },
|
||||
{ key: "Escape", keyCode: 229 },
|
||||
{ key: "Enter", ctrlKey: true, isComposing: true },
|
||||
{ key: "Enter", ctrlKey: true, keyCode: 229 },
|
||||
{ key: "Enter", metaKey: true, isComposing: true },
|
||||
{ key: "Enter", metaKey: true, keyCode: 229 },
|
||||
])("leaves the draft and composing key untouched: %j", async (event) => {
|
||||
const { view, editor, onEditAndRegenerate } = openEditor();
|
||||
await act(async () => {
|
||||
expect(fireEvent.keyDown(editor, event)).toBe(true);
|
||||
});
|
||||
expect(view.getByRole("textbox")).toBe(editor);
|
||||
expect(view.getByDisplayValue("unfinished draft")).toBe(editor);
|
||||
expect(onEditAndRegenerate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("still cancels with Escape outside composition", () => {
|
||||
const { view, editor, onEditAndRegenerate } = openEditor();
|
||||
fireEvent.keyDown(editor, { key: "Escape" });
|
||||
expect(view.queryByRole("textbox")).toBeNull();
|
||||
expect(onEditAndRegenerate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each([{ ctrlKey: true }, { metaKey: true }])(
|
||||
"still submits with modified Enter outside composition: %j",
|
||||
async (modifier) => {
|
||||
const { view, editor, onEditAndRegenerate } = openEditor();
|
||||
await act(async () => {
|
||||
fireEvent.keyDown(editor, { key: "Enter", ...modifier });
|
||||
});
|
||||
expect(onEditAndRegenerate).toHaveBeenCalledExactlyOnceWith(
|
||||
"unfinished draft",
|
||||
);
|
||||
expect(view.queryByRole("textbox")).toBeNull();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("MessageListItem copy-data derivation guard", () => {
|
||||
it("derives no copy data for an assistant row that never renders the toolbar", () => {
|
||||
const message = makeMessage("ai");
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user