From 798628f0b5467af5e62d26250cf4e4849d7c4e7f Mon Sep 17 00:00:00 2001 From: tianma-if Date: Fri, 21 Aug 2026 19:04:42 +0800 Subject: [PATCH] feat(ai): generate with Enter shortcut (#236) --- .../components/dialogs/AiAssistantDialog.tsx | 19 +++++++++++- tests/e2e/ai-custom-prompts.spec.ts | 29 +++++++++++++++++-- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/apps/web/src/components/dialogs/AiAssistantDialog.tsx b/apps/web/src/components/dialogs/AiAssistantDialog.tsx index e0725718..5a1e813e 100644 --- a/apps/web/src/components/dialogs/AiAssistantDialog.tsx +++ b/apps/web/src/components/dialogs/AiAssistantDialog.tsx @@ -491,7 +491,11 @@ export const AiAssistantDialog = ({ ) : ( )} @@ -562,6 +566,19 @@ export const AiAssistantDialog = ({ setCustomInstruction(event.currentTarget.value); clearResult(); }} + onKeyDown={(event) => { + if ( + event.key !== "Enter" + || event.shiftKey + || event.nativeEvent.isComposing + || isGenerating + || isReadingAttachments + ) { + return; + } + event.preventDefault(); + void generate(); + }} placeholder={t("aiAssistant.customInstructionPlaceholder")} maxLength={2_000} /> diff --git a/tests/e2e/ai-custom-prompts.spec.ts b/tests/e2e/ai-custom-prompts.spec.ts index cc476b37..9dfc0b39 100644 --- a/tests/e2e/ai-custom-prompts.spec.ts +++ b/tests/e2e/ai-custom-prompts.spec.ts @@ -27,9 +27,11 @@ const mockAiGeneration = async ( page: Page, replacement: string, onRequest?: (body: Record) => void, + delayMs = 0, ) => { await page.route("**/api/v1/ai/generate", async (route) => { onRequest?.(route.request().postDataJSON() as Record); + if (delayMs > 0) await new Promise((resolve) => setTimeout(resolve, delayMs)); await route.fulfill({ status: 200, contentType: "text/event-stream; charset=utf-8", @@ -338,13 +340,15 @@ test.describe("AI custom prompts", () => { test("keeps a Chinese custom instruction usable while prompts initialize", async ({ page }) => { const memo = await createMemo(page, `e2e-ai-ime-${Date.now()}`, "中文输入状态测试"); let submittedBody: Record | null = null; + let generationRequestCount = 0; await page.route("**/api/v1/ai/prompts*", async (route) => { await new Promise((resolve) => setTimeout(resolve, 800)); await route.continue(); }); await mockAiGeneration(page, "宋词结果", (body) => { + generationRequestCount += 1; submittedBody = body; - }); + }, 300); await ensureAuthenticatedPage(page); await page.getByRole("button", { name: new RegExp(notebookName) }).click(); await page.locator(`[data-memo-id="${memo.id}"]`).locator("button").first().click(); @@ -355,12 +359,31 @@ test.describe("AI custom prompts", () => { await textarea.fill("写个宋词"); const generateButton = dialog.getByRole("button", { name: "生成", exact: true }); await expect(generateButton).toBeEnabled(); - await generateButton.click(); + await expect(generateButton.locator("kbd")).toHaveText("Enter"); + + await textarea.press("Shift+Enter"); + await textarea.pressSequentially("不要参考原笔记"); + await expect(textarea).toHaveValue("写个宋词\n不要参考原笔记"); + + await textarea.evaluate((element) => { + element.dispatchEvent(new KeyboardEvent("keydown", { + bubbles: true, + isComposing: true, + key: "Enter", + })); + }); + expect(submittedBody).toBeNull(); + + await textarea.press("Enter"); + await expect.poll(() => generationRequestCount).toBe(1); + await textarea.press("Enter"); + await page.waitForTimeout(50); + expect(generationRequestCount).toBe(1); await expect(dialog.getByText("宋词结果", { exact: true })).toBeVisible(); expect(submittedBody).toMatchObject({ action: "custom", - instruction: "写个宋词", + instruction: "写个宋词\n不要参考原笔记", }); });