From 276c02f341b08c7bd7a258e7ec62c4a9ff8f2c72 Mon Sep 17 00:00:00 2001 From: wellCh4n Date: Mon, 19 May 2025 23:17:18 +0800 Subject: [PATCH] feat: Variable click jumps to source node (#13623) --- .../components/base/prompt-editor/hooks.ts | 8 ++-- .../workflow-variable-block/component.tsx | 29 ++++++++++++++ .../components/base/prompt-editor/types.ts | 2 +- .../components/input-support-select-var.tsx | 3 ++ .../nodes/_base/components/prompt/editor.tsx | 3 ++ .../nodes/_base/components/variable-tag.tsx | 36 +++++++++++++++-- .../variable/var-reference-picker.tsx | 40 +++++++++++++++++-- .../condition-list/condition-input.tsx | 3 ++ web/app/components/workflow/types.ts | 2 + 9 files changed, 116 insertions(+), 10 deletions(-) diff --git a/web/app/components/base/prompt-editor/hooks.ts b/web/app/components/base/prompt-editor/hooks.ts index c9e4cc129c..87119f8b49 100644 --- a/web/app/components/base/prompt-editor/hooks.ts +++ b/web/app/components/base/prompt-editor/hooks.ts @@ -74,9 +74,11 @@ export const useSelectOrDelete: UseSelectOrDeleteHandler = (nodeKey: string, com ) const handleSelect = useCallback((e: MouseEvent) => { - e.stopPropagation() - clearSelection() - setSelected(true) + if (!e.metaKey && !e.ctrlKey) { + e.stopPropagation() + clearSelection() + setSelected(true) + } }, [setSelected, clearSelection]) useEffect(() => { diff --git a/web/app/components/base/prompt-editor/plugins/workflow-variable-block/component.tsx b/web/app/components/base/prompt-editor/plugins/workflow-variable-block/component.tsx index 50ff29633a..731841f423 100644 --- a/web/app/components/base/prompt-editor/plugins/workflow-variable-block/component.tsx +++ b/web/app/components/base/prompt-editor/plugins/workflow-variable-block/component.tsx @@ -1,5 +1,6 @@ import { memo, + useCallback, useEffect, useState, } from 'react' @@ -13,6 +14,7 @@ import { RiErrorWarningFill, RiMoreLine, } from '@remixicon/react' +import { useReactFlow, useStoreApi } from 'reactflow' import { useSelectOrDelete } from '../../hooks' import type { WorkflowNodesMap } from './node' import { WorkflowVariableBlockNode } from './node' @@ -66,6 +68,9 @@ const WorkflowVariableBlockComponent = ({ const isChatVar = isConversationVar(variables) const isException = isExceptionVariable(varName, node?.type) + const reactflow = useReactFlow() + const store = useStoreApi() + useEffect(() => { if (!editor.hasNodes([WorkflowVariableBlockNode])) throw new Error('WorkflowVariableBlockPlugin: WorkflowVariableBlock not registered on editor') @@ -83,6 +88,26 @@ const WorkflowVariableBlockComponent = ({ ) }, [editor]) + const handleVariableJump = useCallback(() => { + const workflowContainer = document.getElementById('workflow-container') + const { + clientWidth, + clientHeight, + } = workflowContainer! + + const { + setViewport, + } = reactflow + const { transform } = store.getState() + const zoom = transform[2] + const position = node.position + setViewport({ + x: (clientWidth - 400 - node.width! * zoom) / 2 - position!.x * zoom, + y: (clientHeight - node.height! * zoom) / 2 - position!.y * zoom, + zoom: transform[2], + }) + }, [node, reactflow, store]) + const Item = (
{ + e.stopPropagation() + handleVariableJump() + }} ref={ref} > {!isEnv && !isChatVar && ( diff --git a/web/app/components/base/prompt-editor/types.ts b/web/app/components/base/prompt-editor/types.ts index 0f09fb2473..e82ec1da16 100644 --- a/web/app/components/base/prompt-editor/types.ts +++ b/web/app/components/base/prompt-editor/types.ts @@ -64,7 +64,7 @@ export type GetVarType = (payload: { export type WorkflowVariableBlockType = { show?: boolean variables?: NodeOutPutVar[] - workflowNodesMap?: Record> + workflowNodesMap?: Record> onInsert?: () => void onDelete?: () => void getVarType?: GetVarType diff --git a/web/app/components/workflow/nodes/_base/components/input-support-select-var.tsx b/web/app/components/workflow/nodes/_base/components/input-support-select-var.tsx index aab14bb6f9..a741629da8 100644 --- a/web/app/components/workflow/nodes/_base/components/input-support-select-var.tsx +++ b/web/app/components/workflow/nodes/_base/components/input-support-select-var.tsx @@ -91,6 +91,9 @@ const Editor: FC = ({ acc[node.id] = { title: node.data.title, type: node.data.type, + width: node.width, + height: node.height, + position: node.position, } if (node.data.type === BlockEnum.Start) { acc.sys = { diff --git a/web/app/components/workflow/nodes/_base/components/prompt/editor.tsx b/web/app/components/workflow/nodes/_base/components/prompt/editor.tsx index c6233ff377..0a7ebc2a09 100644 --- a/web/app/components/workflow/nodes/_base/components/prompt/editor.tsx +++ b/web/app/components/workflow/nodes/_base/components/prompt/editor.tsx @@ -259,6 +259,9 @@ const Editor: FC = ({ acc[node.id] = { title: node.data.title, type: node.data.type, + width: node.width, + height: node.height, + position: node.position, } if (node.data.type === BlockEnum.Start) { acc.sys = { diff --git a/web/app/components/workflow/nodes/_base/components/variable-tag.tsx b/web/app/components/workflow/nodes/_base/components/variable-tag.tsx index 83b07715fe..d73a3d4924 100644 --- a/web/app/components/workflow/nodes/_base/components/variable-tag.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable-tag.tsx @@ -1,5 +1,5 @@ -import { useMemo } from 'react' -import { useNodes } from 'reactflow' +import { useCallback, useMemo } from 'react' +import { useNodes, useReactFlow, useStoreApi } from 'reactflow' import { capitalize } from 'lodash-es' import { useTranslation } from 'react-i18next' import { RiErrorWarningFill } from '@remixicon/react' @@ -48,12 +48,42 @@ const VariableTag = ({ const variableName = isSystemVar(valueSelector) ? valueSelector.slice(0).join('.') : valueSelector.slice(1).join('.') const isException = isExceptionVariable(variableName, node?.data.type) + const reactflow = useReactFlow() + const store = useStoreApi() + + const handleVariableJump = useCallback(() => { + const workflowContainer = document.getElementById('workflow-container') + const { + clientWidth, + clientHeight, + } = workflowContainer! + + const { + setViewport, + } = reactflow + const { transform } = store.getState() + const zoom = transform[2] + const position = node.position + setViewport({ + x: (clientWidth - 400 - node.width! * zoom) / 2 - position!.x * zoom, + y: (clientHeight - node.height! * zoom) / 2 - position!.y * zoom, + zoom: transform[2], + }) + }, [node, reactflow, store]) + const { t } = useTranslation() return (
+ )} + onClick={(e) => { + if (e.metaKey || e.ctrlKey) { + e.stopPropagation() + handleVariableJump() + } + }} + > {(!isEnv && !isChatVar && <> {node && ( <> diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx b/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx index 789da34f9d..e9825cd44a 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx @@ -9,7 +9,7 @@ import { RiMoreLine, } from '@remixicon/react' import produce from 'immer' -import { useStoreApi } from 'reactflow' +import { useReactFlow, useStoreApi } from 'reactflow' import RemoveButton from '../remove-button' import useAvailableVarList from '../../hooks/use-available-var-list' import VarReferencePopup from './var-reference-popup' @@ -111,6 +111,9 @@ const VarReferencePicker: FC = ({ passedInAvailableNodes, filterVar, }) + + const reactflow = useReactFlow() + const startNode = availableNodes.find((node: any) => { return node.data.type === BlockEnum.Start }) @@ -172,7 +175,11 @@ const VarReferencePicker: FC = ({ if (isSystemVar(value as ValueSelector)) return startNode?.data - return getNodeInfoById(availableNodes, outputVarNodeId)?.data + const node = getNodeInfoById(availableNodes, outputVarNodeId)?.data + return { + ...node, + id: outputVarNodeId, + } }, [value, hasValue, isConstant, isIterationVar, iterationNode, availableNodes, outputVarNodeId, startNode, isLoopVar, loopNode]) const isShowAPart = (value as ValueSelector).length > 2 @@ -237,6 +244,28 @@ const VarReferencePicker: FC = ({ onChange([], varKindType) }, [onChange, varKindType]) + const handleVariableJump = useCallback((nodeId: string) => { + const currentNodeIndex = availableNodes.findIndex(node => node.id === nodeId) + const currentNode = availableNodes[currentNodeIndex] + + const workflowContainer = document.getElementById('workflow-container') + const { + clientWidth, + clientHeight, + } = workflowContainer! + const { + setViewport, + } = reactflow + const { transform } = store.getState() + const zoom = transform[2] + const position = currentNode.position + setViewport({ + x: (clientWidth - 400 - currentNode.width! * zoom) / 2 - position.x * zoom, + y: (clientHeight - currentNode.height! * zoom) / 2 - position.y * zoom, + zoom: transform[2], + }) + }, [availableNodes, reactflow, store]) + const type = getCurrentVariableType({ parentNode: isInIteration ? iterationNode : loopNode, valueSelector: value as ValueSelector, @@ -357,7 +386,12 @@ const VarReferencePicker: FC = ({ ? ( <> {isShowNodeName && !isEnv && !isChatVar && ( -
+
{ + if (e.metaKey || e.ctrlKey) { + e.stopPropagation() + handleVariableJump(outputVarNode?.id) + } + }}>
{outputVarNode?.type && = { type: BlockEnum width?: number height?: number + position?: XYPosition _loopLength?: number _loopIndex?: number isInLoop?: boolean