From 7d80cb6d9593188b2f8e764cef5a244d8c0217f9 Mon Sep 17 00:00:00 2001 From: Joel Date: Wed, 16 Jul 2025 11:24:10 +0800 Subject: [PATCH] feat: current block --- .../configuration/config/automatic/types.ts | 4 + .../base/prompt-editor/constants.tsx | 2 + .../plugins/current-block/component.tsx | 24 ++++++ .../current-block-replacement-block.tsx | 62 +++++++++++++++ .../plugins/current-block/index.tsx | 66 ++++++++++++++++ .../plugins/current-block/node.tsx | 78 +++++++++++++++++++ .../components/base/prompt-editor/types.ts | 8 ++ 7 files changed, 244 insertions(+) create mode 100644 web/app/components/app/configuration/config/automatic/types.ts create mode 100644 web/app/components/base/prompt-editor/plugins/current-block/component.tsx create mode 100644 web/app/components/base/prompt-editor/plugins/current-block/current-block-replacement-block.tsx create mode 100644 web/app/components/base/prompt-editor/plugins/current-block/index.tsx create mode 100644 web/app/components/base/prompt-editor/plugins/current-block/node.tsx diff --git a/web/app/components/app/configuration/config/automatic/types.ts b/web/app/components/app/configuration/config/automatic/types.ts new file mode 100644 index 0000000000..cc97569499 --- /dev/null +++ b/web/app/components/app/configuration/config/automatic/types.ts @@ -0,0 +1,4 @@ +export enum GeneratorType { + prompt = 'prompt', + code = 'code', +} diff --git a/web/app/components/base/prompt-editor/constants.tsx b/web/app/components/base/prompt-editor/constants.tsx index 31fbc0abb4..d694cab2de 100644 --- a/web/app/components/base/prompt-editor/constants.tsx +++ b/web/app/components/base/prompt-editor/constants.tsx @@ -3,6 +3,8 @@ import { SupportUploadFileTypes, type ValueSelector } from '../../workflow/types export const CONTEXT_PLACEHOLDER_TEXT = '{{#context#}}' export const HISTORY_PLACEHOLDER_TEXT = '{{#histories#}}' export const QUERY_PLACEHOLDER_TEXT = '{{#query#}}' +export const CURRENT_PLACEHOLDER_TEXT = '{{#current#}}' + export const PRE_PROMPT_PLACEHOLDER_TEXT = '{{#pre_prompt#}}' export const UPDATE_DATASETS_EVENT_EMITTER = 'prompt-editor-context-block-update-datasets' export const UPDATE_HISTORY_EVENT_EMITTER = 'prompt-editor-history-block-update-role' diff --git a/web/app/components/base/prompt-editor/plugins/current-block/component.tsx b/web/app/components/base/prompt-editor/plugins/current-block/component.tsx new file mode 100644 index 0000000000..2c4b678d0d --- /dev/null +++ b/web/app/components/base/prompt-editor/plugins/current-block/component.tsx @@ -0,0 +1,24 @@ +import type { FC } from 'react' +import { File05 } from '@/app/components/base/icons/src/vender/solid/files' +import { GeneratorType } from '@/app/components/app/configuration/config/automatic/types' + +type CurrentBlockComponentProps = { + nodeKey: string + generatorType: GeneratorType +} + +const CurrentBlockComponent: FC = ({ + generatorType, +}) => { + return ( +
+ +
{generatorType === GeneratorType.prompt ? 'current_prompt' : 'current_code'}
+
+ ) +} + +export default CurrentBlockComponent diff --git a/web/app/components/base/prompt-editor/plugins/current-block/current-block-replacement-block.tsx b/web/app/components/base/prompt-editor/plugins/current-block/current-block-replacement-block.tsx new file mode 100644 index 0000000000..8ca56b0cf4 --- /dev/null +++ b/web/app/components/base/prompt-editor/plugins/current-block/current-block-replacement-block.tsx @@ -0,0 +1,62 @@ +import { + memo, + useCallback, + useEffect, +} from 'react' +import { $applyNodeReplacement } from 'lexical' +import { mergeRegister } from '@lexical/utils' +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' +import { decoratorTransform } from '../../utils' +import { CURRENT_PLACEHOLDER_TEXT } from '../../constants' +import type { CurrentBlockType } from '../../types' +import { + $createCurrentBlockNode, + CurrentBlockNode, +} from './node' +import { CustomTextNode } from '../custom-text/node' + +const REGEX = new RegExp(CURRENT_PLACEHOLDER_TEXT) + +const CurrentBlockReplacementBlock = ({ + generatorType, + onInsert, +}: CurrentBlockType) => { + const [editor] = useLexicalComposerContext() + + useEffect(() => { + if (!editor.hasNodes([CurrentBlockNode])) + throw new Error('CurrentBlockNodePlugin: CurrentBlockNode not registered on editor') + }, [editor]) + + const createCurrentBlockNode = useCallback((): CurrentBlockNode => { + if (onInsert) + onInsert() + return $applyNodeReplacement($createCurrentBlockNode(generatorType)) + }, [onInsert, generatorType]) + + const getMatch = useCallback((text: string) => { + const matchArr = REGEX.exec(text) + + if (matchArr === null) + return null + + const startOffset = matchArr.index + const endOffset = startOffset + CURRENT_PLACEHOLDER_TEXT.length + return { + end: endOffset, + start: startOffset, + } + }, []) + + useEffect(() => { + REGEX.lastIndex = 0 + return mergeRegister( + editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getMatch, createCurrentBlockNode)), + ) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + return null +} + +export default memo(CurrentBlockReplacementBlock) diff --git a/web/app/components/base/prompt-editor/plugins/current-block/index.tsx b/web/app/components/base/prompt-editor/plugins/current-block/index.tsx new file mode 100644 index 0000000000..0b9ed6c5a4 --- /dev/null +++ b/web/app/components/base/prompt-editor/plugins/current-block/index.tsx @@ -0,0 +1,66 @@ +import { + memo, + useEffect, +} from 'react' +import { + $insertNodes, + COMMAND_PRIORITY_EDITOR, + createCommand, +} from 'lexical' +import { mergeRegister } from '@lexical/utils' +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' +import type { CurrentBlockType } from '../../types' +import { + $createCurrentBlockNode, + CurrentBlockNode, +} from './node' + +export const INSERT_CURRENT_BLOCK_COMMAND = createCommand('INSERT_CURRENT_BLOCK_COMMAND') +export const DELETE_CURRENT_BLOCK_COMMAND = createCommand('DELETE_CURRENT_BLOCK_COMMAND') + +const CurrentBlock = memo(({ + generatorType, + onInsert, + onDelete, +}: CurrentBlockType) => { + const [editor] = useLexicalComposerContext() + + useEffect(() => { + if (!editor.hasNodes([CurrentBlockNode])) + throw new Error('CURRENTBlockPlugin: CURRENTBlock not registered on editor') + + return mergeRegister( + editor.registerCommand( + INSERT_CURRENT_BLOCK_COMMAND, + () => { + const currentBlockNode = $createCurrentBlockNode(generatorType) + + $insertNodes([currentBlockNode]) + + if (onInsert) + onInsert() + + return true + }, + COMMAND_PRIORITY_EDITOR, + ), + editor.registerCommand( + DELETE_CURRENT_BLOCK_COMMAND, + () => { + if (onDelete) + onDelete() + + return true + }, + COMMAND_PRIORITY_EDITOR, + ), + ) + }, [editor, generatorType, onDelete, onInsert]) + + return null +}) +CurrentBlock.displayName = 'CurrentBlock' + +export { CurrentBlock } +export { CurrentBlockNode } from './node' +export { default as CurrentBlockReplacementBlock } from './current-block-replacement-block' diff --git a/web/app/components/base/prompt-editor/plugins/current-block/node.tsx b/web/app/components/base/prompt-editor/plugins/current-block/node.tsx new file mode 100644 index 0000000000..6f2a3a1751 --- /dev/null +++ b/web/app/components/base/prompt-editor/plugins/current-block/node.tsx @@ -0,0 +1,78 @@ +import type { LexicalNode, NodeKey, SerializedLexicalNode } from 'lexical' +import { DecoratorNode } from 'lexical' +import CurrentBlockComponent from './component' +import type { GeneratorType } from '@/app/components/app/configuration/config/automatic/types' + +export type SerializedNode = SerializedLexicalNode & { generatorType: GeneratorType; } + +export class CurrentBlockNode extends DecoratorNode { + __generatorType: GeneratorType + static getType(): string { + return 'current-block' + } + + static clone(node: CurrentBlockNode): CurrentBlockNode { + return new CurrentBlockNode(node.__generatorType) + } + + isInline(): boolean { + return true + } + + constructor(generatorType: GeneratorType, key?: NodeKey) { + super(key) + + this.__generatorType = generatorType + } + + 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 ( + + ) + } + + getGeneratorType(): GeneratorType { + const self = this.getLatest() + return self.__generatorType + } + + static importJSON(serializedNode: SerializedNode): CurrentBlockNode { + const node = $createCurrentBlockNode(serializedNode.generatorType) + + return node + } + + exportJSON(): SerializedNode { + return { + type: 'current-block', + version: 1, + generatorType: this.getGeneratorType(), + } + } + + getTextContent(): string { + return '{{#current#}}' + } +} +export function $createCurrentBlockNode(type: GeneratorType): CurrentBlockNode { + return new CurrentBlockNode(type) +} + +export function $isCurrentBlockNode( + node: CurrentBlockNode | LexicalNode | null | undefined, +): boolean { + return node instanceof CurrentBlockNode +} diff --git a/web/app/components/base/prompt-editor/types.ts b/web/app/components/base/prompt-editor/types.ts index e82ec1da16..88ae5fc3ab 100644 --- a/web/app/components/base/prompt-editor/types.ts +++ b/web/app/components/base/prompt-editor/types.ts @@ -1,3 +1,4 @@ +import type { GeneratorType } from '../../app/configuration/config/automatic/types' import type { Type } from '../../workflow/nodes/llm/types' import type { Dataset } from './plugins/context-block' import type { RoleName } from './plugins/history-block' @@ -75,3 +76,10 @@ export type MenuTextMatch = { matchingString: string replaceableString: string } + +export type CurrentBlockType = { + show?: boolean + generatorType: GeneratorType + onInsert?: () => void + onDelete?: () => void +}