diff --git a/blocksuite/framework/std/src/event/control/keyboard.ts b/blocksuite/framework/std/src/event/control/keyboard.ts index 27d5257b36..418e01c9c8 100644 --- a/blocksuite/framework/std/src/event/control/keyboard.ts +++ b/blocksuite/framework/std/src/event/control/keyboard.ts @@ -59,6 +59,21 @@ export class KeyboardControl { private composition = false; + private readonly _press = (event: KeyboardEvent) => { + if (!this._shouldTrigger(event)) { + return; + } + const keyboardEventState = new KeyboardEventState({ + event, + composing: this.composition, + }); + + this._dispatcher.run( + 'keyPress', + this._createContext(event, keyboardEventState) + ); + }; + constructor(private readonly _dispatcher: UIEventDispatcher) {} private _createContext(event: Event, keyboardState: KeyboardEventState) { @@ -105,6 +120,11 @@ export class KeyboardControl { listen() { this._dispatcher.disposables.addFromEvent(document, 'keydown', this._down); this._dispatcher.disposables.addFromEvent(document, 'keyup', this._up); + this._dispatcher.disposables.addFromEvent( + document, + 'keypress', + this._press + ); this._dispatcher.disposables.addFromEvent( document, 'compositionstart', diff --git a/blocksuite/framework/std/src/event/dispatcher.ts b/blocksuite/framework/std/src/event/dispatcher.ts index 1a1ba2fce6..c9fdd41ce8 100644 --- a/blocksuite/framework/std/src/event/dispatcher.ts +++ b/blocksuite/framework/std/src/event/dispatcher.ts @@ -46,6 +46,7 @@ const eventNames = [ 'keyDown', 'keyUp', + 'keyPress', 'selectionChange', 'compositionStart', diff --git a/packages/frontend/core/src/blocksuite/ai/entries/space/setup-space.ts b/packages/frontend/core/src/blocksuite/ai/entries/space/setup-space.ts index 461a1d6d33..8d1a414cd6 100644 --- a/packages/frontend/core/src/blocksuite/ai/entries/space/setup-space.ts +++ b/packages/frontend/core/src/blocksuite/ai/entries/space/setup-space.ts @@ -5,20 +5,31 @@ import { AIProvider } from '../../provider'; import type { AffineAIPanelWidget } from '../../widgets/ai-panel/ai-panel'; export function setupSpaceAIEntry(panel: AffineAIPanelWidget) { - panel.handleEvent('keyDown', ctx => { + // Background: The keydown event triggered by a space may originate from: + // 1. Normal space insertion + // 2. Space triggered by input method confirming candidate words + // In scenarios like (2), some browsers (see [ISSUE](https://github.com/toeverything/AFFiNE/issues/11541)) + // and input method callbacks produce events identical to scenario (1), + // making it impossible to distinguish between the two. + // + // To fix this, the space-activated AI listener uses the `keypress` event: + // In scenario 2, `event.which !== 32` (may be `30430` or other values) can be used to differentiate from scenario 1. + panel.handleEvent('keyPress', ctx => { const host = panel.host; const keyboardState = ctx.get('keyboardState'); + const event = keyboardState.raw; if ( AIProvider.actions.chat && - keyboardState.raw.key === ' ' && - !keyboardState.raw.isComposing + event.key === ' ' && + event.which === 32 && + !event.isComposing ) { const selection = host.selection.find(TextSelection); if (selection && selection.isCollapsed() && selection.from.index === 0) { const block = host.view.getBlock(selection.blockId); if (!block?.model?.text || block.model.text?.length > 0) return; - keyboardState.raw.preventDefault(); + event.preventDefault(); handleInlineAskAIAction(host); } } diff --git a/tests/affine-cloud-copilot/e2e/basic/guidance.spec.ts b/tests/affine-cloud-copilot/e2e/basic/guidance.spec.ts new file mode 100644 index 0000000000..d79342d5c0 --- /dev/null +++ b/tests/affine-cloud-copilot/e2e/basic/guidance.spec.ts @@ -0,0 +1,18 @@ +import { expect } from '@playwright/test'; + +import { test } from '../base/base-test'; + +test.describe('AIBasic/Guidance', () => { + test.beforeEach(async ({ page, utils }) => { + await utils.testUtils.setupTestEnvironment(page); + }); + + test('should show AI panel when space is pressed on empty paragraph', async ({ + page, + utils, + }) => { + await utils.editor.focusToEditor(page); + await page.keyboard.press('Space'); + await expect(page.locator('affine-ai-panel-widget')).toBeVisible(); + }); +});