From 71a511a470b55a03c3bf8a27cb2ac193e6d5ab61 Mon Sep 17 00:00:00 2001 From: Joel Date: Tue, 26 Aug 2025 15:50:53 +0800 Subject: [PATCH] feat: can insert hitl node by / --- .../components/base/prompt-editor/index.tsx | 5 +- .../plugins/hitl-input-block/index.tsx | 50 +++++++++++++++++++ .../plugins/hitl-input-block/input-field.tsx | 10 +++- .../plugins/hitl-input-block/node.tsx | 4 +- .../plugins/shortcuts-popup-plugin/index.tsx | 10 +++- .../components/add-input-field.tsx | 27 ++++++++++ .../human-input/components/form-content.tsx | 32 +++++++++++- 7 files changed, 129 insertions(+), 9 deletions(-) create mode 100644 web/app/components/workflow/nodes/human-input/components/add-input-field.tsx diff --git a/web/app/components/base/prompt-editor/index.tsx b/web/app/components/base/prompt-editor/index.tsx index 6a1c89f144..daa4baf2f9 100644 --- a/web/app/components/base/prompt-editor/index.tsx +++ b/web/app/components/base/prompt-editor/index.tsx @@ -4,6 +4,7 @@ import type { FC } from 'react' import React, { useEffect, useState } from 'react' import type { EditorState, + LexicalCommand, } from 'lexical' import { $getRoot, @@ -119,7 +120,7 @@ export type PromptEditorProps = { errorMessageBlock?: ErrorMessageBlockType lastRunBlock?: LastRunBlockType isSupportFileVar?: boolean - shortcutPopups?: Array<{ hotkey: Hotkey; Popup: React.ComponentType<{ onClose: () => void }> }> + shortcutPopups?: Array<{ hotkey: Hotkey; Popup: React.ComponentType<{ onClose: () => void, onInsert: (command: LexicalCommand, params: any[]) => void }> }> } const PromptEditor: FC = ({ @@ -229,7 +230,7 @@ const PromptEditor: FC = ({ /> {shortcutPopups?.map(({ hotkey, Popup }, idx) => ( - {closePortal => } + {(closePortal, onInsert) => } ))} { if (!editor.hasNodes([HITLInputNode])) throw new Error('HITLInputBlockPlugin: HITLInputBlock not registered on editor') + return mergeRegister( + editor.registerCommand( + INSERT_HITL_INPUT_BLOCK_COMMAND, + (nodeProps: HITLNodeProps) => { + const { + variableName, + nodeId, + nodeTitle, + formInputs, + onFormInputsChange, + onFormInputItemRename, + onFormInputItemRemove, + } = nodeProps + const currentHITLNode = $createHITLInputNode( + variableName, + nodeId, + nodeTitle, + formInputs, + onFormInputsChange, + onFormInputItemRename, + onFormInputItemRemove, + ) + + $insertNodes([currentHITLNode]) + + if (onInsert) + onInsert() + + return true + }, + COMMAND_PRIORITY_EDITOR, + ), + editor.registerCommand( + DELETE_HITL_INPUT_BLOCK_COMMAND, + () => { + if (onDelete) + onDelete() + + return true + }, + COMMAND_PRIORITY_EDITOR, + ), + ) }, [editor, onInsert, onDelete]) return null diff --git a/web/app/components/base/prompt-editor/plugins/hitl-input-block/input-field.tsx b/web/app/components/base/prompt-editor/plugins/hitl-input-block/input-field.tsx index 29ecb69b34..a715fe6a6b 100644 --- a/web/app/components/base/prompt-editor/plugins/hitl-input-block/input-field.tsx +++ b/web/app/components/base/prompt-editor/plugins/hitl-input-block/input-field.tsx @@ -8,16 +8,22 @@ import { getKeyboardKeyNameBySystem } from '@/app/components/workflow/utils' import type { FormInputItem, FormInputItemPlaceholder } from '@/app/components/workflow/nodes/human-input/types' import PrePopulate from './pre-populate' import produce from 'immer' +import { InputVarType } from '@/app/components/workflow/types' const i18nPrefix = 'workflow.nodes.humanInput.insertInputField' type Props = { nodeId: string isEdit: boolean - payload: FormInputItem + payload?: FormInputItem onChange: (newPayload: FormInputItem) => void onCancel: () => void } +const defaultPayload: FormInputItem = { + type: InputVarType.paragraph, + output_variable_name: '', + placeholder: { type: 'const', selector: [], value: '' }, +} const InputField: React.FC = ({ nodeId, isEdit, @@ -26,7 +32,7 @@ const InputField: React.FC = ({ onCancel, }) => { const { t } = useTranslation() - const [tempPayload, setTempPayload] = useState(payload) + const [tempPayload, setTempPayload] = useState(payload || defaultPayload) const handleSave = useCallback(() => { onChange(tempPayload) }, [tempPayload]) diff --git a/web/app/components/base/prompt-editor/plugins/hitl-input-block/node.tsx b/web/app/components/base/prompt-editor/plugins/hitl-input-block/node.tsx index 336dcf064b..491b5b74f2 100644 --- a/web/app/components/base/prompt-editor/plugins/hitl-input-block/node.tsx +++ b/web/app/components/base/prompt-editor/plugins/hitl-input-block/node.tsx @@ -3,7 +3,7 @@ import { DecoratorNode } from 'lexical' import HILTInputBlockComponent from './component' import type { FormInputItem } from '@/app/components/workflow/nodes/human-input/types' -export type SerializedNode = SerializedLexicalNode & { +export type HITLNodeProps = { variableName: string nodeId: string nodeTitle: string @@ -13,6 +13,8 @@ export type SerializedNode = SerializedLexicalNode & { onFormInputItemRemove: (varName: string) => void } +export type SerializedNode = SerializedLexicalNode & HITLNodeProps + export class HITLInputNode extends DecoratorNode { __variableName: string __nodeId: string diff --git a/web/app/components/base/prompt-editor/plugins/shortcuts-popup-plugin/index.tsx b/web/app/components/base/prompt-editor/plugins/shortcuts-popup-plugin/index.tsx index b3506f48c2..19ff08e536 100644 --- a/web/app/components/base/prompt-editor/plugins/shortcuts-popup-plugin/index.tsx +++ b/web/app/components/base/prompt-editor/plugins/shortcuts-popup-plugin/index.tsx @@ -7,6 +7,7 @@ import { useState, } from 'react' import { createPortal } from 'react-dom' +import type { LexicalCommand } from 'lexical' import { $getSelection, $isRangeSelection, @@ -24,7 +25,7 @@ export type Hotkey = string | string[] | string[][] | ((e: KeyboardEvent) => boo type ShortcutPopupPluginProps = { hotkey?: Hotkey - children?: React.ReactNode | ((close: () => void) => React.ReactNode) + children?: React.ReactNode | ((close: () => void, onInsert: (command: LexicalCommand, params: any[]) => void) => React.ReactNode) className?: string style?: React.CSSProperties container?: Element | null @@ -261,6 +262,11 @@ export default function ShortcutsPopupPlugin({ return () => document.removeEventListener('mousedown', onMouseDown, true) }, [open, closePortal]) + const handleInsert = useCallback((command: LexicalCommand, params: any) => { + editor.dispatchCommand(command, params) + closePortal() + }, [editor, closePortal]) + if (!open || !containerEl) return null @@ -274,7 +280,7 @@ export default function ShortcutsPopupPlugin({ )} style={{ top: `${position.top}px`, left: `${position.left}px`, ...style }} > - {typeof children === 'function' ? children(closePortal) : (children ?? SHORTCUTS_EMPTY_CONTENT)} + {typeof children === 'function' ? children(closePortal, handleInsert) : (children ?? SHORTCUTS_EMPTY_CONTENT)} , containerEl, ) diff --git a/web/app/components/workflow/nodes/human-input/components/add-input-field.tsx b/web/app/components/workflow/nodes/human-input/components/add-input-field.tsx new file mode 100644 index 0000000000..8767537aa6 --- /dev/null +++ b/web/app/components/workflow/nodes/human-input/components/add-input-field.tsx @@ -0,0 +1,27 @@ +'use client' +import InputField from '@/app/components/base/prompt-editor/plugins/hitl-input-block/input-field' +import type { FC } from 'react' +import React from 'react' +import type { FormInputItem } from '../types' + +type Props = { + nodeId: string + onSave: (newPayload: FormInputItem) => void + onCancel: () => void +} + +const AddInputField: FC = ({ + nodeId, + onSave, + onCancel, +}) => { + return ( + + ) +} +export default React.memo(AddInputField) diff --git a/web/app/components/workflow/nodes/human-input/components/form-content.tsx b/web/app/components/workflow/nodes/human-input/components/form-content.tsx index 316d56e5c6..facd0e5efe 100644 --- a/web/app/components/workflow/nodes/human-input/components/form-content.tsx +++ b/web/app/components/workflow/nodes/human-input/components/form-content.tsx @@ -7,28 +7,31 @@ import { BlockEnum } from '../../../types' import { useWorkflowVariableType } from '../../../hooks' import { useTranslation } from 'react-i18next' import type { FormInputItem } from '../types' +import AddInputField from './add-input-field' +import { INSERT_HITL_INPUT_BLOCK_COMMAND } from '@/app/components/base/prompt-editor/plugins/hitl-input-block' +import type { LexicalCommand } from 'lexical' type Props = { nodeId: string + nodeTitle: string value: string onChange: (value: string) => void formInputs: FormInputItem[] onFormInputsChange: (payload: FormInputItem[]) => void onFormInputItemRename: (payload: FormInputItem, oldName: string) => void onFormInputItemRemove: (varName: string) => void - nodeTitle: string editorKey: number } const FormContent: FC = ({ nodeId, + nodeTitle, value, onChange, formInputs, onFormInputsChange, onFormInputItemRename, onFormInputItemRemove, - nodeTitle, editorKey, }) => { const { t } = useTranslation() @@ -43,6 +46,21 @@ const FormContent: FC = ({ const getVarType = useWorkflowVariableType() + const handleInsertHITLNode = (onInsert: (command: LexicalCommand, params: any) => void) => { + return (payload: FormInputItem) => { + // todo insert into form inputs + onInsert(INSERT_HITL_INPUT_BLOCK_COMMAND, { + variableName: payload.output_variable_name, + nodeId, + nodeTitle, + formInputs, + onFormInputsChange, + onFormInputItemRename, + onFormInputItemRemove, + }) + } + } + return (
= ({ }, {}), }} editable + shortcutPopups={[{ + hotkey: ['mod', '/'], + Popup: ({ onClose, onInsert }) => ( + + ), + }]} />
)