diff --git a/web/app/components/base/prompt-editor/plugins/hitl-input-block/component.tsx b/web/app/components/base/prompt-editor/plugins/hitl-input-block/component.tsx new file mode 100644 index 0000000000..71a1a4450a --- /dev/null +++ b/web/app/components/base/prompt-editor/plugins/hitl-input-block/component.tsx @@ -0,0 +1,35 @@ +import type { FC } from 'react' +import { useTranslation } from 'react-i18next' +import { useSelectOrDelete } from '../../hooks' +import { DELETE_HITL_INPUT_BLOCK_COMMAND } from './' +import { UserEdit02 } from '@/app/components/base/icons/src/vender/solid/users' + +type QueryBlockComponentProps = { + nodeKey: string + nodeName: string + varName: string +} + +const HITLInputComponent: FC = ({ + nodeKey, + nodeName, + varName, +}) => { + const { t } = useTranslation() + const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_HITL_INPUT_BLOCK_COMMAND) + + return ( +
+ + {nodeName}/{varName} +
+ ) +} + +export default HITLInputComponent diff --git a/web/app/components/base/prompt-editor/plugins/hitl-input-block/hitl-input-block-replacement-block.tsx b/web/app/components/base/prompt-editor/plugins/hitl-input-block/hitl-input-block-replacement-block.tsx new file mode 100644 index 0000000000..24cbe7c756 --- /dev/null +++ b/web/app/components/base/prompt-editor/plugins/hitl-input-block/hitl-input-block-replacement-block.tsx @@ -0,0 +1,62 @@ +import { + memo, + useCallback, + useEffect, +} from 'react' +import type { TextNode } from 'lexical' +import { $applyNodeReplacement } from 'lexical' +import { mergeRegister } from '@lexical/utils' +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' +import { decoratorTransform } from '../../utils' +import type { QueryBlockType } from '../../types' +import { $createHITLInputNode } from './node' +import { + QueryBlockNode, +} from '../query-block/node' +import { CustomTextNode } from '../custom-text/node' +import { HITL_INPUT_REG } from '@/config' + +const REGEX = new RegExp(HITL_INPUT_REG) + +const QueryBlockReplacementBlock = ({ + onInsert, +}: QueryBlockType) => { + const [editor] = useLexicalComposerContext() + + useEffect(() => { + if (!editor.hasNodes([QueryBlockNode])) + throw new Error('QueryBlockNodePlugin: QueryBlockNode not registered on editor') + }, [editor]) + + const createHITLInputBlockNode = useCallback((textNode: TextNode): QueryBlockNode => { + if (onInsert) + onInsert() + const varName = textNode.getTextContent().split('.')[1] + return $applyNodeReplacement($createHITLInputNode(varName)) + }, [onInsert]) + + const getMatch = useCallback((text: string) => { + const matchArr = REGEX.exec(text) + + if (matchArr === null) + return null + + const startOffset = matchArr.index + const endOffset = startOffset + matchArr[0].length + return { + end: endOffset, + start: startOffset, + } + }, []) + + useEffect(() => { + REGEX.lastIndex = 0 + return mergeRegister( + editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getMatch, createHITLInputBlockNode)), + ) + }, []) + + return null +} + +export default memo(QueryBlockReplacementBlock) diff --git a/web/app/components/base/prompt-editor/plugins/hitl-input-block/index.tsx b/web/app/components/base/prompt-editor/plugins/hitl-input-block/index.tsx new file mode 100644 index 0000000000..d8442399df --- /dev/null +++ b/web/app/components/base/prompt-editor/plugins/hitl-input-block/index.tsx @@ -0,0 +1,42 @@ +import { + memo, + useEffect, +} from 'react' +import { + createCommand, +} from 'lexical' +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' +import type { QueryBlockType } from '../../types' +import { + HITLInputNode, +} from './node' + +export const INSERT_HITL_INPUT_BLOCK_COMMAND = createCommand('INSERT_HITL_INPUT_BLOCK_COMMAND') +export const DELETE_HITL_INPUT_BLOCK_COMMAND = createCommand('DELETE_HITL_INPUT_BLOCK_COMMAND') + +export type HITLInputProps = { + onInsert?: () => void + onDelete?: () => void +} +const HITLInputBlock = memo(({ + onInsert, + onDelete, +}: QueryBlockType) => { + const [editor] = useLexicalComposerContext() + + useEffect(() => { + if (!editor.hasNodes([HITLInputNode])) + throw new Error('HITLInputBlockPlugin: HITLInputBlock not registered on editor') + }, [editor, onInsert, onDelete]) + + // TODO + // const createHITLBlockNode = useCallback + + return null +}) + +HITLInputBlock.displayName = 'HITLInputBlock' + +export { HITLInputBlock } +export { HITLInputNode } from './node' +export { default as HITLInputBlockReplacementBlock } from './hitl-input-block-replacement-block' 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 new file mode 100644 index 0000000000..4f7ec1265b --- /dev/null +++ b/web/app/components/base/prompt-editor/plugins/hitl-input-block/node.tsx @@ -0,0 +1,76 @@ +import type { LexicalNode, NodeKey, SerializedLexicalNode } from 'lexical' +import { DecoratorNode } from 'lexical' +import HILTInputBlockComponent from './component' + +export type SerializedNode = SerializedLexicalNode & { + variableName: string +} + +export class HITLInputNode extends DecoratorNode { + __variableName: string + + static getType(): string { + return 'hitl-input-block' + } + + getVariableName(): string { + const self = this.getLatest() + return self.__variableName + } + + static clone(node: HITLInputNode): HITLInputNode { + return new HITLInputNode(node.__variableName, node.__key) + } + + isInline(): boolean { + return true + } + + constructor(varName: string, key?: NodeKey) { + super(key) + + this.__variableName = varName + } + + createDOM(): HTMLElement { + const div = document.createElement('div') + div.classList.add('inline-flex', 'items-center', 'align-middle') + return div + } + + updateDOM(): false { + return false + } + + decorate(): React.JSX.Element { + return + } + + static importJSON(serializedNode: SerializedNode): HITLInputNode { + const node = $createHITLInputNode(serializedNode.variableName) + + return node + } + + exportJSON(): SerializedNode { + return { + type: 'hitl-input-block', + version: 1, + variableName: this.getVariableName(), + } + } + + getTextContent(): string { + return `{{#$outputs.${this.getVariableName()}#}}` + } +} + +export function $createHITLInputNode(variableName: string): HITLInputNode { + return new HITLInputNode(variableName) +} + +export function $isHITLInputNode( + node: HITLInputNode | LexicalNode | null | undefined, +): node is HITLInputNode { + return node instanceof HITLInputNode +}