From 4c76a5f57bc85b682acc6a5aea9de3ff4dba34f4 Mon Sep 17 00:00:00 2001 From: Joel Date: Wed, 16 Jul 2025 18:23:59 +0800 Subject: [PATCH] feat: support choose last run --- .../base/prompt-editor/constants.tsx | 1 + .../components/base/prompt-editor/index.tsx | 22 ++++++ .../plugins/component-picker-block/hooks.tsx | 17 ++++- .../plugins/component-picker-block/index.tsx | 14 +++- .../plugins/last-run-block/component.tsx | 40 +++++++++++ .../plugins/last-run-block/index.tsx | 65 ++++++++++++++++++ .../last-run-block-replacement-block.tsx | 61 +++++++++++++++++ .../plugins/last-run-block/node.tsx | 67 +++++++++++++++++++ .../components/base/prompt-editor/types.ts | 6 ++ .../variable/var-reference-vars.tsx | 2 + 10 files changed, 291 insertions(+), 4 deletions(-) create mode 100644 web/app/components/base/prompt-editor/plugins/last-run-block/component.tsx create mode 100644 web/app/components/base/prompt-editor/plugins/last-run-block/index.tsx create mode 100644 web/app/components/base/prompt-editor/plugins/last-run-block/last-run-block-replacement-block.tsx create mode 100644 web/app/components/base/prompt-editor/plugins/last-run-block/node.tsx diff --git a/web/app/components/base/prompt-editor/constants.tsx b/web/app/components/base/prompt-editor/constants.tsx index 2fd0a62e95..c3313ba406 100644 --- a/web/app/components/base/prompt-editor/constants.tsx +++ b/web/app/components/base/prompt-editor/constants.tsx @@ -5,6 +5,7 @@ export const HISTORY_PLACEHOLDER_TEXT = '{{#histories#}}' export const QUERY_PLACEHOLDER_TEXT = '{{#query#}}' export const CURRENT_PLACEHOLDER_TEXT = '{{#current#}}' export const ERROR_MESSAGE_PLACEHOLDER_TEXT = '{{#error_message#}}' +export const LAST_RUN_PLACEHOLDER_TEXT = '{{#last_run#}}' export const PRE_PROMPT_PLACEHOLDER_TEXT = '{{#pre_prompt#}}' export const UPDATE_DATASETS_EVENT_EMITTER = 'prompt-editor-context-block-update-datasets' diff --git a/web/app/components/base/prompt-editor/index.tsx b/web/app/components/base/prompt-editor/index.tsx index 0a21587bc6..de83a29f7f 100644 --- a/web/app/components/base/prompt-editor/index.tsx +++ b/web/app/components/base/prompt-editor/index.tsx @@ -49,6 +49,12 @@ import { ErrorMessageBlockNode, ErrorMessageBlockReplacementBlock, } from './plugins/error-message-block' +import { + LastRunBlock, + LastRunBlockNode, + LastRunReplacementBlock, +} from './plugins/last-run-block' + import VariableBlock from './plugins/variable-block' import VariableValueBlock from './plugins/variable-value-block' import { VariableValueBlockNode } from './plugins/variable-value-block/node' @@ -62,6 +68,7 @@ import type { ErrorMessageBlockType, ExternalToolBlockType, HistoryBlockType, + LastRunBlockType, QueryBlockType, VariableBlockType, WorkflowVariableBlockType, @@ -95,6 +102,7 @@ export type PromptEditorProps = { workflowVariableBlock?: WorkflowVariableBlockType currentBlock?: CurrentBlockType errorMessageBlock?: ErrorMessageBlockType + lastRunBlock?: LastRunBlockType isSupportFileVar?: boolean } @@ -124,6 +132,9 @@ const PromptEditor: FC = ({ errorMessageBlock = { show: true, }, + lastRunBlock = { + show: true, + }, isSupportFileVar, }) => { const { eventEmitter } = useEventEmitterContextContext() @@ -143,6 +154,7 @@ const PromptEditor: FC = ({ VariableValueBlockNode, CurrentBlockNode, ErrorMessageBlockNode, + LastRunBlockNode, // LastRunBlockNode is used for error message block replacement ], editorState: textToEditorState(value || ''), onError: (error: Error) => { @@ -204,6 +216,7 @@ const PromptEditor: FC = ({ workflowVariableBlock={workflowVariableBlock} currentBlock={currentBlock} errorMessageBlock={errorMessageBlock} + lastRunBlock={lastRunBlock} isSupportFileVar={isSupportFileVar} /> = ({ workflowVariableBlock={workflowVariableBlock} currentBlock={currentBlock} errorMessageBlock={errorMessageBlock} + lastRunBlock={lastRunBlock} isSupportFileVar={isSupportFileVar} /> { @@ -274,6 +288,14 @@ const PromptEditor: FC = ({ ) } + { + lastRunBlock?.show && ( + <> + + + + ) + } { isSupportFileVar && ( diff --git a/web/app/components/base/prompt-editor/plugins/component-picker-block/hooks.tsx b/web/app/components/base/prompt-editor/plugins/component-picker-block/hooks.tsx index 51689e1790..2032f22ce9 100644 --- a/web/app/components/base/prompt-editor/plugins/component-picker-block/hooks.tsx +++ b/web/app/components/base/prompt-editor/plugins/component-picker-block/hooks.tsx @@ -8,6 +8,7 @@ import type { ErrorMessageBlockType, ExternalToolBlockType, HistoryBlockType, + LastRunBlockType, QueryBlockType, VariableBlockType, WorkflowVariableBlockType, @@ -272,6 +273,7 @@ export const useOptions = ( workflowVariableBlockType?: WorkflowVariableBlockType, currentBlockType?: CurrentBlockType, errorMessageBlockType?: ErrorMessageBlockType, + lastRunBlockType?: LastRunBlockType, queryString?: string, ) => { const promptOptions = usePromptOptions(contextBlock, queryBlock, historyBlock) @@ -295,6 +297,19 @@ export const useOptions = ( ], }) } + if(lastRunBlockType?.show && res.findIndex(v => v.nodeId === 'last_run') === -1) { + res.unshift({ + nodeId: 'last_run', + title: 'last_run', + isFlat: true, + vars: [ + { + variable: 'last_run', + type: VarType.object, + }, + ], + }) + } if(currentBlockType?.show && res.findIndex(v => v.nodeId === 'current') === -1) { const title = currentBlockType.generatorType === 'prompt' ? 'current_prompt' : 'current_code' res.unshift({ @@ -310,7 +325,7 @@ export const useOptions = ( }) } return res - }, [workflowVariableBlockType?.show, workflowVariableBlockType?.variables, errorMessageBlockType?.show, currentBlockType?.show, currentBlockType?.generatorType]) + }, [workflowVariableBlockType?.show, workflowVariableBlockType?.variables, errorMessageBlockType?.show, lastRunBlockType?.show, currentBlockType?.show, currentBlockType?.generatorType]) return useMemo(() => { return { diff --git a/web/app/components/base/prompt-editor/plugins/component-picker-block/index.tsx b/web/app/components/base/prompt-editor/plugins/component-picker-block/index.tsx index 73b81f957f..cffb2762c2 100644 --- a/web/app/components/base/prompt-editor/plugins/component-picker-block/index.tsx +++ b/web/app/components/base/prompt-editor/plugins/component-picker-block/index.tsx @@ -21,6 +21,7 @@ import type { ErrorMessageBlockType, ExternalToolBlockType, HistoryBlockType, + LastRunBlockType, QueryBlockType, VariableBlockType, WorkflowVariableBlockType, @@ -37,6 +38,7 @@ import { KEY_ESCAPE_COMMAND } from 'lexical' import { INSERT_CURRENT_BLOCK_COMMAND } from '../current-block' import { GeneratorType } from '@/app/components/app/configuration/config/automatic/types' import { INSERT_ERROR_MESSAGE_BLOCK_COMMAND } from '../error-message-block' +import { INSERT_LAST_RUN_BLOCK_COMMAND } from '../last-run-block' type ComponentPickerProps = { triggerString: string @@ -48,6 +50,7 @@ type ComponentPickerProps = { workflowVariableBlock?: WorkflowVariableBlockType currentBlock?: CurrentBlockType errorMessageBlock?: ErrorMessageBlockType + lastRunBlock?: LastRunBlockType isSupportFileVar?: boolean } const ComponentPicker = ({ @@ -60,6 +63,7 @@ const ComponentPicker = ({ workflowVariableBlock, currentBlock, errorMessageBlock, + lastRunBlock, isSupportFileVar, }: ComponentPickerProps) => { const { eventEmitter } = useEventEmitterContextContext() @@ -98,6 +102,7 @@ const ComponentPicker = ({ workflowVariableBlock, currentBlock, errorMessageBlock, + lastRunBlock, ) const onSelectOption = useCallback( @@ -125,10 +130,13 @@ const ComponentPicker = ({ }) const isFlat = variables.length === 1 if(isFlat) { - if(variables[0] === 'current') + const varName = variables[0] + if(varName === 'current') editor.dispatchCommand(INSERT_CURRENT_BLOCK_COMMAND, currentBlock?.generatorType) - else if (variables[0] === 'error_message') + else if (varName === 'error_message') editor.dispatchCommand(INSERT_ERROR_MESSAGE_BLOCK_COMMAND, null) + else if (varName === 'last_run') + editor.dispatchCommand(INSERT_LAST_RUN_BLOCK_COMMAND, null) } else if (variables[1] === 'sys.query' || variables[1] === 'sys.files') { editor.dispatchCommand(INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND, [variables[1]]) @@ -226,7 +234,7 @@ const ComponentPicker = ({ } ) - }, [allFlattenOptions.length, workflowVariableBlock?.show, refs, isPositioned, floatingStyles, queryString, workflowVariableOptions, handleSelectWorkflowVariable, handleClose, isSupportFileVar]) + }, [allFlattenOptions.length, workflowVariableBlock?.show, floatingStyles, isPositioned, refs, workflowVariableOptions, isSupportFileVar, handleClose, currentBlock?.generatorType, handleSelectWorkflowVariable, queryString]) return ( = ({ + nodeKey, +}) => { + const [editor] = useLexicalComposerContext() + const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_LAST_RUN_COMMAND) + + useEffect(() => { + if (!editor.hasNodes([LastRunBlockNode])) + throw new Error('WorkflowVariableBlockPlugin: WorkflowVariableBlock not registered on editor') + }, [editor]) + + return ( +
{ + e.stopPropagation() + }} + ref={ref} + > + +
last_run
+
+ ) +} + +export default LastRunBlockComponent diff --git a/web/app/components/base/prompt-editor/plugins/last-run-block/index.tsx b/web/app/components/base/prompt-editor/plugins/last-run-block/index.tsx new file mode 100644 index 0000000000..8bbbb8d4dd --- /dev/null +++ b/web/app/components/base/prompt-editor/plugins/last-run-block/index.tsx @@ -0,0 +1,65 @@ +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 { LastRunBlockType } from '../../types' +import { + $createLastRunBlockNode, + LastRunBlockNode, +} from './node' + +export const INSERT_LAST_RUN_BLOCK_COMMAND = createCommand('INSERT_LAST_RUN_BLOCK_COMMAND') +export const DELETE_LAST_RUN_COMMAND = createCommand('DELETE_LAST_RUN_COMMAND') + +const LastRunBlock = memo(({ + onInsert, + onDelete, +}: LastRunBlockType) => { + const [editor] = useLexicalComposerContext() + + useEffect(() => { + if (!editor.hasNodes([LastRunBlockNode])) + throw new Error('Last_RunBlockPlugin: Last_RunBlock not registered on editor') + + return mergeRegister( + editor.registerCommand( + INSERT_LAST_RUN_BLOCK_COMMAND, + () => { + const Node = $createLastRunBlockNode() + + $insertNodes([Node]) + + if (onInsert) + onInsert() + + return true + }, + COMMAND_PRIORITY_EDITOR, + ), + editor.registerCommand( + DELETE_LAST_RUN_COMMAND, + () => { + if (onDelete) + onDelete() + + return true + }, + COMMAND_PRIORITY_EDITOR, + ), + ) + }, [editor, onDelete, onInsert]) + + return null +}) +LastRunBlock.displayName = 'LastRunBlock' + +export { LastRunBlock } +export { LastRunBlockNode } from './node' +export { default as LastRunReplacementBlock } from './last-run-block-replacement-block' diff --git a/web/app/components/base/prompt-editor/plugins/last-run-block/last-run-block-replacement-block.tsx b/web/app/components/base/prompt-editor/plugins/last-run-block/last-run-block-replacement-block.tsx new file mode 100644 index 0000000000..9d28828016 --- /dev/null +++ b/web/app/components/base/prompt-editor/plugins/last-run-block/last-run-block-replacement-block.tsx @@ -0,0 +1,61 @@ +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 { LAST_RUN_PLACEHOLDER_TEXT } from '../../constants' +import type { LastRunBlockType } from '../../types' +import { + $createLastRunBlockNode, + LastRunBlockNode, +} from './node' +import { CustomTextNode } from '../custom-text/node' + +const REGEX = new RegExp(LAST_RUN_PLACEHOLDER_TEXT) + +const LastRunReplacementBlock = ({ + onInsert, +}: LastRunBlockType) => { + const [editor] = useLexicalComposerContext() + + useEffect(() => { + if (!editor.hasNodes([LastRunBlockNode])) + throw new Error('LastRunMessageBlockNodePlugin: LastRunMessageBlockNode not registered on editor') + }, [editor]) + + const createLastRunBlockNode = useCallback((): LastRunBlockNode => { + if (onInsert) + onInsert() + return $applyNodeReplacement($createLastRunBlockNode()) + }, [onInsert]) + + const getMatch = useCallback((text: string) => { + const matchArr = REGEX.exec(text) + + if (matchArr === null) + return null + + const startOffset = matchArr.index + const endOffset = startOffset + LAST_RUN_PLACEHOLDER_TEXT.length + return { + end: endOffset, + start: startOffset, + } + }, []) + + useEffect(() => { + REGEX.lastIndex = 0 + return mergeRegister( + editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getMatch, createLastRunBlockNode)), + ) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + return null +} + +export default memo(LastRunReplacementBlock) diff --git a/web/app/components/base/prompt-editor/plugins/last-run-block/node.tsx b/web/app/components/base/prompt-editor/plugins/last-run-block/node.tsx new file mode 100644 index 0000000000..5f61c3138b --- /dev/null +++ b/web/app/components/base/prompt-editor/plugins/last-run-block/node.tsx @@ -0,0 +1,67 @@ +import type { LexicalNode, NodeKey, SerializedLexicalNode } from 'lexical' +import { DecoratorNode } from 'lexical' +import LastRunBlockComponent from './component' + +export type SerializedNode = SerializedLexicalNode + +export class LastRunBlockNode extends DecoratorNode { + static getType(): string { + return 'last-run-block' + } + + static clone(node: LastRunBlockNode): LastRunBlockNode { + return new LastRunBlockNode(node.getKey()) + } + + isInline(): boolean { + return true + } + + constructor(key?: NodeKey) { + super(key) + } + + 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(): LastRunBlockNode { + const node = $createLastRunBlockNode() + + return node + } + + exportJSON(): SerializedNode { + return { + type: 'last-run-block', + version: 1, + } + } + + getTextContent(): string { + return '{{#last_run#}}' + } +} +export function $createLastRunBlockNode(): LastRunBlockNode { + return new LastRunBlockNode() +} + +export function $isLastRunBlockNode( + node: LastRunBlockNode | LexicalNode | null | undefined, +): boolean { + return node instanceof LastRunBlockNode +} diff --git a/web/app/components/base/prompt-editor/types.ts b/web/app/components/base/prompt-editor/types.ts index 939d8afd89..45ae2e2af7 100644 --- a/web/app/components/base/prompt-editor/types.ts +++ b/web/app/components/base/prompt-editor/types.ts @@ -89,3 +89,9 @@ export type ErrorMessageBlockType = { onInsert?: () => void onDelete?: () => void } + +export type LastRunBlockType = { + show?: boolean + onInsert?: () => void + onDelete?: () => void +} diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx b/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx index 0bdcd529fb..688ffc6f9e 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx @@ -85,6 +85,8 @@ const Item: FC = ({ return case 'error_message': return + default: + return } }, [isFlat, isInCodeGeneratorInstructionEditor, itemData.variable])