Browse Source

fix: Fixed the conflict between the AIChatInput send hotkey and its subdefined extended hotkey (#3034)

YyumeiZhang 1 week ago
parent
commit
409c06c27e

+ 18 - 2
content/ai/aiChatInput/index-en-US.md

@@ -885,6 +885,13 @@ render(<RenderTopSlot />);
 
 Rich text areas support custom extensions. For implementation details, see [Tiptap Custom Extensions](https://tiptap.dev/docs/editor/extensions/custom-extensions/create-new). Custom extensions can be added to the AIChatInput component using the `extensions` API. If you add a custom extension, you must configure the corresponding transformation rules in `transformer` to ensure that the data returned in `onContentChange` matches your expectations.
 
+When adding a custom extension, please note the following:
+
+- Please add the `isCustomSlot` property to your custom extension. This property is related to the cursor height before and after the custom extension.
+- Since `AIChatInput` uses `Enter` as the send hotkey, if your custom extension uses `Enter` as a shortcut, you need to manually configure `AIChatInput.allowHotKeySend` in `editor.storage` to indicate whether the hotkey should be used by AIChatInput for sending, in order to avoid hotkey conflicts.
+
+An example of a custom extension definition and related notes is as follows:
+
 ```jsx live=true dir="column" noInline=true
 import React from 'react';
 import { Node, mergeAttributes } from '@tiptap/core';
@@ -1050,8 +1057,11 @@ const updatePosition = (editor, element) => {
 const suggestion = {
     items: () => FirstLevel,
     command: ({ editor, range, props }) => {
-        const { item } = props;
-        editor.chain().focus().insertContentAt(range, {
+        const { item, allowHotKeySend } = props;
+        if (typeof allowHotKeySend === 'boolean') {
+            editor.storage.SemiAIChatInput.allowHotKeySend = allowHotKeySend;
+        }
+        item && editor.chain().focus().insertContentAt(range, {
             type: 'referSlot',
             attrs: {
                 type: item.type,
@@ -1152,6 +1162,12 @@ class MentionList extends React.Component {
         this.selectItem = this.selectItem.bind(this);
         this.onKeyDown = this.onKeyDown.bind(this);
         this.renderItem = this.renderItem.bind(this);
+        // When the options panel is rendered, the Enter shortcut should be used in the options panel, not for sending with AIChatInput.
+        props.command({ allowHotKeySend: false });
+    }
+    componentWillUnmount() {
+        // If the options panel is unmounted, the Enter shortcut should be used to send AIChatInput.
+        this.props.command({ allowHotKeySend: true });
     }
     upHandler() {
         const { selectedIndex, filterOptions } = this.state;

+ 17 - 2
content/ai/aiChatInput/index.md

@@ -963,6 +963,12 @@ render(<RenderTopSlot />);
 
 富文本区域可以自定义扩展,自定义扩展的实现可参考 [Tiptap 自定义扩展](https://tiptap.dev/docs/editor/extensions/custom-extensions/create-new)。通过 `extensions` API 可将自定义扩展添加到 `AIChatInput` 组件中。如果添加了自定义扩展,需要在 `transformer` 中添加对应的转换规则, 以保证在 `onContentChange` 中得到的该节点数据符合用户预期。
 
+添加自定义扩展时有以下注意事项:
+- 请在自定义扩展中添加 `isCustomSlot` 的属性,该属性和自定义扩展前后的光标高度有关
+- 由于 `AIChatInput` 使用 `Enter` 作为发送热键,如果自定义扩展有使用 `Enter` 作为快捷操作,需要自行设置 `editor.storage` 中的 `AIChatInput.allowHotKeySend` 用于表示热键是否应该被 AIChatInput 用于发送,避免热键冲突
+
+自定义扩展定义及注意事项的示例如下:
+
 ```jsx live=true dir="column" noInline=true
 import React from 'react';
 import { Node, mergeAttributes } from '@tiptap/core';
@@ -1129,8 +1135,11 @@ const updatePosition = (editor, element) => {
 const suggestion = {
     items: () => FirstLevel,
     command: ({ editor, range, props }) => {
-        const { item } = props;
-        editor.chain().focus().insertContentAt(range, {
+        const { item, allowHotKeySend } = props;
+        if (typeof allowHotKeySend === 'boolean') {
+            editor.storage.SemiAIChatInput.allowHotKeySend = allowHotKeySend;
+        }
+        item && editor.chain().focus().insertContentAt(range, {
             type: 'referSlot',
             attrs: {
                 type: item.type,
@@ -1230,6 +1239,12 @@ class MentionList extends React.Component {
         this.selectItem = this.selectItem.bind(this);
         this.onKeyDown = this.onKeyDown.bind(this);
         this.renderItem = this.renderItem.bind(this);
+        // 选项面板渲染,则 Enter 快捷键应该用于选项面板中,不能用于 AIChatInput 的发送,
+        props.command({ allowHotKeySend: false });
+    }
+    componentWillUnmount() {
+        // 选项面板卸载,则 Enter 快捷键应该用于 AIChatInput 的发送
+        this.props.command({ allowHotKeySend: true });
     }
     upHandler() {
         const { selectedIndex, filterOptions } = this.state;

+ 4 - 2
packages/semi-foundation/aiChatInput/foundation.ts

@@ -1,6 +1,6 @@
 import BaseFoundation, { DefaultAdapter } from '../base/foundation';
 import { Attachment, BaseSkill, Suggestion, Reference, Content, LeftMenuChangeProps, MessageContent } from './interface';
-import { isNumber, isString } from 'lodash';
+import { get, isNumber, isString } from 'lodash';
 import { cssClasses } from './constants';
 import { findSkillSlotInString, getSkillSlotString, transformJSONResult } from './utils';
 
@@ -345,7 +345,9 @@ export default class AIChatInputFoundation extends BaseFoundation<AIChatInputAda
         if ((suggestionVisible || skillVisible) && ['ArrowUp', 'ArrowDown', 'Enter'].includes(event.key)) {
             return true;
         }
-        if (event.key === 'Enter' && !event.shiftKey) {
+        const editor = this._adapter.getEditor() ?? {};
+        const allowHotKeySend = get(editor, 'storage.SemiAIChatInput.allowHotKeySend');
+        if (event.key === 'Enter' && !event.shiftKey && allowHotKeySend) {
             this.handleSend();
             return true;
         }

+ 7 - 3
packages/semi-ui/aiChatInput/_story/suggestion/index.tsx

@@ -28,9 +28,13 @@ const suggestion = {
     // items: ({ query }: any) => FirstLevel,
     items: () => FirstLevel,
 
-    command: ({ editor, range, props }: any) => {
-        const { item } = props;
-        editor.chain().focus().insertContentAt(range, {
+    command: (obj: any) => {
+        const { editor, range, props } = obj;
+        const { item, allowHotKeySend } = props;
+        if (typeof allowHotKeySend === 'boolean') {
+            editor.storage.SemiAIChatInput.allowHotKeySend = allowHotKeySend;
+        }
+        item && editor.chain().focus().insertContentAt(range, {
             type: 'referSlot',
             attrs: {
                 type: item.type,

+ 5 - 0
packages/semi-ui/aiChatInput/_story/suggestion/mentionList.tsx

@@ -69,6 +69,11 @@ class MentionList extends Component<any, any> {
             options: FirstLevel,
             filterOptions: FirstLevel,
         };
+        props.command({ allowHotKeySend: false });
+    }
+
+    componentWillUnmount(): void {
+        this.props.command({ allowHotKeySend: true });
     }
 
     upHandler = () => {

+ 34 - 0
packages/semi-ui/aiChatInput/extension/statusExtension.tsx

@@ -0,0 +1,34 @@
+import { Extension, RawCommands } from "@tiptap/core";
+
+/**
+ * 为什么需要这个扩展?
+ * 此扩展用于管理和 SemiAIChatInput 有关的状态,避免 SemiAIChatInput 和其他扩展的行为冲突,举个例子:
+ * 自定义的扩展需要通过 enter 实现快捷按键操作,会和 SemiAIChatInput 的发送热键有冲突,
+ * 因此通过 editor 的 storage 存储 allowHotKeySend 的状态,扩展可以去设置这些状态,提示 SemiAIChatInput 是否需要响应热键
+ * Why is this extension needed?
+ * This extension is used to manage the state related to SemiAIChatInput and avoid behavioral conflicts between 
+ * SemiAIChatInput and other extensions. For example:
+ * Custom extensions require shortcut key operations via Enter, which conflicts with SemiAIChatInput's send hotkey.
+ * Therefore, by storing the allowHotKeySend state in the editor's storage, 
+ * the extension can set these states to indicate whether SemiAIChatInput needs to respond to hotkeys.
+ */
+const SemiStatusExtension = Extension.create({
+    name: 'SemiAIChatInput',
+    addStorage() {
+        return { 
+            allowHotKeySend: true,
+        };
+    },
+
+    addCommands() {
+        return {
+            setAllowHotKeySendForSemiAIChatInput(allow: boolean) {
+                return ({ storage }) => {
+                    storage.SemiAIChatInput.allowHotKeySend = allow;
+                };
+            }
+        } as Partial<RawCommands>;
+    }
+});
+
+export default SemiStatusExtension;

+ 2 - 0
packages/semi-ui/aiChatInput/richTextInput.tsx

@@ -14,6 +14,7 @@ import { Content as TiptapContent } from "@tiptap/core";
 import { cssClasses } from '@douyinfe/semi-foundation/aiChatInput/constants';
 import { EditorView } from '@tiptap/pm/view';
 import { handleCompositionEndLogic, handlePasteLogic, handleTextInputLogic, handleZeroWidthCharLogic } from './extension/plugins';
+import SemiStatusExtension from './extension/statusExtension';
 
 const PREFIX = cssClasses.PREFIX;
 
@@ -58,6 +59,7 @@ export default (props: {
             Placeholder.configure({
                 placeholder: placeholder,
             }),
+            SemiStatusExtension,
             ...extensions,
         ];
     }, [extensions, placeholder]);