diff --git a/web/app/components/app/configuration/config-var/config-modal/index.tsx b/web/app/components/app/configuration/config-var/config-modal/index.tsx index 86786b4e35..649e598b21 100644 --- a/web/app/components/app/configuration/config-var/config-modal/index.tsx +++ b/web/app/components/app/configuration/config-var/config-modal/index.tsx @@ -16,12 +16,23 @@ import { type InputVar, InputVarType } from '@/app/components/workflow/types' import Modal from '@/app/components/base/modal' import Switch from '@/app/components/base/switch' +export enum ChangeType { + changeVarName = 'changeVarName', +} + +export type MoreInfo = { + type: ChangeType + payload?: { + beforeKey: string + afterKey: string + } +} export type IConfigModalProps = { isCreate?: boolean payload?: InputVar isShow: boolean onClose: () => void - onConfirm: (newValue: InputVar) => void + onConfirm: (newValue: InputVar, moreInfo?: MoreInfo) => void } const inputClassName = 'w-full px-3 text-sm leading-9 text-gray-900 border-0 rounded-lg grow h-9 bg-gray-50 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200' @@ -39,7 +50,6 @@ const ConfigModal: FC = ({ const { type, label, variable, options, max_length } = tempPayload const isStringInput = type === InputVarType.textInput || type === InputVarType.paragraph - const handlePayloadChange = useCallback((key: string) => { return (value: any) => { setTempPayload((prev) => { @@ -52,6 +62,12 @@ const ConfigModal: FC = ({ }, []) const handleConfirm = () => { + const moreInfo = tempPayload.variable === payload?.variable + ? undefined + : { + type: ChangeType.changeVarName, + payload: { beforeKey: payload?.variable || '', afterKey: tempPayload.variable }, + } if (!tempPayload.variable) { Toast.notify({ type: 'error', message: t('appDebug.variableConig.errorMsg.varNameRequired') }) return @@ -61,7 +77,7 @@ const ConfigModal: FC = ({ return } if (isStringInput || type === InputVarType.number) { - onConfirm(tempPayload) + onConfirm(tempPayload, moreInfo) } else { if (options?.length === 0) { @@ -81,7 +97,7 @@ const ConfigModal: FC = ({ Toast.notify({ type: 'error', message: t('appDebug.variableConig.errorMsg.optionRepeat') }) return } - onConfirm(tempPayload) + onConfirm(tempPayload, moreInfo) } } diff --git a/web/app/components/workflow/hooks/use-workflow.ts b/web/app/components/workflow/hooks/use-workflow.ts index 83bca57393..0a7f71973c 100644 --- a/web/app/components/workflow/hooks/use-workflow.ts +++ b/web/app/components/workflow/hooks/use-workflow.ts @@ -27,6 +27,7 @@ import { import type { Edge, Node, + ValueSelector, } from '../types' import { BlockEnum, @@ -41,6 +42,7 @@ import { START_INITIAL_POSITION, SUPPORT_OUTPUT_VARS_NODE, } from '../constants' +import { findUsedVarNodes, updateNodeVars } from '../nodes/_base/components/variable/utils' import { useNodesExtraData, useNodesInitialData, @@ -55,7 +57,6 @@ import { } from '@/service/workflow' import { fetchCollectionList } from '@/service/tools' import I18n from '@/context/i18n' - export const useIsChatMode = () => { const appDetail = useAppStore(s => s.appDetail) @@ -211,6 +212,25 @@ export const useWorkflow = () => { return uniqBy(list, 'id') }, [store]) + const handleOutVarRenameChange = useCallback((nodeId: string, oldValeSelector: ValueSelector, newVarSelector: ValueSelector) => { + const { getNodes, setNodes } = store.getState() + const afterNodes = getAfterNodesInSameBranch(nodeId) + const effectNodes = findUsedVarNodes(oldValeSelector, afterNodes) + console.log(effectNodes) + if (effectNodes.length > 0) { + // debugger + const newNodes = getNodes().map((node) => { + if (effectNodes.find(n => n.id === node.id)) + return updateNodeVars(node, oldValeSelector, newVarSelector) + + return node + }) + setNodes(newNodes) + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [store]) + const isValidConnection = useCallback(({ source, target }: Connection) => { const { getNodes } = store.getState() const nodes = getNodes() @@ -294,6 +314,7 @@ export const useWorkflow = () => { getTreeLeafNodes, getBeforeNodesInSameBranch, getAfterNodesInSameBranch, + handleOutVarRenameChange, isValidConnection, formatTimeFromNow, getValidTreeNodes, diff --git a/web/app/components/workflow/nodes/_base/components/variable/utils.ts b/web/app/components/workflow/nodes/_base/components/variable/utils.ts index 666a340944..c0b887febc 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/utils.ts +++ b/web/app/components/workflow/nodes/_base/components/variable/utils.ts @@ -1,7 +1,18 @@ +import produce from 'immer' import type { CodeNodeType } from '../../../code/types' +import type { EndNodeType } from '../../../end/types' +import type { AnswerNodeType } from '../../../answer/types' +import type { LLMNodeType } from '../../../llm/types' +import type { KnowledgeRetrievalNodeType } from '../../../knowledge-retrieval/types' +import type { IfElseNodeType } from '../../../if-else/types' +import type { TemplateTransformNodeType } from '../../../template-transform/types' +import type { QuestionClassifierNodeType } from '../../../question-classifier/types' +import type { HttpNodeType } from '../../../http/types' +import type { ToolNodeType } from '../../../tool/types' +import { VarType as VarKindType } from '../../../tool/types' import { BlockEnum, InputVarType, VarType } from '@/app/components/workflow/types' import type { StartNodeType } from '@/app/components/workflow/nodes/start/types' -import type { NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types' +import type { Node, NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types' import type { VariableAssignerNodeType } from '@/app/components/workflow/nodes/variable-assigner/types' import { CHAT_QUESTION_CLASSIFIER_OUTPUT_STRUCT, @@ -199,3 +210,116 @@ export const getVarType = (value: ValueSelector, availableNodes: any[], isChatMo return type } } + +const getNodeUsedVars = (node: Node): ValueSelector[] => { + const { data } = node + const { type } = data + let res: ValueSelector[] = [] + switch (type) { + case BlockEnum.End: { + res = (data as EndNodeType).outputs?.map((output) => { + return output.value_selector + }) + break + } + case BlockEnum.Answer: { + res = (data as AnswerNodeType).variables?.map((v) => { + return v.value_selector + }) + break + } + case BlockEnum.LLM: { + const inputVars = (data as LLMNodeType)?.variables.map((v) => { + return v.value_selector + }) + + const contextVar = (data as LLMNodeType).context?.variable_selector ? [(data as LLMNodeType).context?.variable_selector] : [] + res = [...inputVars, ...contextVar] + break + } + case BlockEnum.KnowledgeRetrieval: { + res = [(data as KnowledgeRetrievalNodeType).query_variable_selector] + break + } + case BlockEnum.IfElse: { + res = (data as IfElseNodeType).conditions?.map((c) => { + return c.variable_selector + }) + break + } + case BlockEnum.Code: { + res = (data as CodeNodeType).variables?.map((v) => { + return v.value_selector + }) + break + } + case BlockEnum.TemplateTransform: { + res = (data as TemplateTransformNodeType).variables?.map((v: any) => { + return v.value_selector + }) + break + } + case BlockEnum.QuestionClassifier: { + res = [(data as QuestionClassifierNodeType).query_variable_selector] + break + } + case BlockEnum.HttpRequest: { + res = (data as HttpNodeType).variables?.map((v) => { + return v.value_selector + }) + break + } + case BlockEnum.Tool: { + res = (data as ToolNodeType).tool_parameters?.filter((v) => { + return v.variable_type === VarKindType.static + }).map((v) => { + return v.value_selector || [] + }) + break + } + + case BlockEnum.VariableAssigner: { + res = (data as VariableAssignerNodeType)?.variables + } + } + return res || [] +} + +export const findUsedVarNodes = (varSelector: ValueSelector, availableNodes: Node[]): Node[] => { + const res: Node[] = [] + availableNodes.forEach((node) => { + const vars = getNodeUsedVars(node) + if (vars.find(v => v.join('.') === varSelector.join('.'))) + res.push(node) + }) + return res +} + +export const updateNodeVars = (oldNode: Node, oldVarSelector: ValueSelector, newVarSelector: ValueSelector): Node => { + const newNode = produce(oldNode, (draft: any) => { + const { data } = draft + const { type } = data + switch (type) { + case BlockEnum.End: { + if ((data as EndNodeType).outputs) { + (data as EndNodeType).outputs = (data as EndNodeType).outputs.map((output) => { + if (output.value_selector.join('.') === oldVarSelector.join('.')) + output.value_selector = newVarSelector + return output + }) + } + break + } + case BlockEnum.Answer: { + if ((data as AnswerNodeType).variables) { + (data as AnswerNodeType).variables = (data as AnswerNodeType).variables.map((v) => { + if (v.value_selector.join('.') === oldVarSelector.join('.')) + v.value_selector = newVarSelector + return v + }) + } + } + } + }) + return newNode +} 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 d4d0a3388b..a36e13a291 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 @@ -187,10 +187,10 @@ const VarReferencePicker: FC = ({ // 8(left/right-padding) + 14(icon) + 4 + 14 + 2 = 42 + 5 buff const availableWidth = triggerWidth - 47 const [maxNodeNameWidth, maxVarNameWidth, maxTypeWidth] = (() => { - const totalTextLength = ((outputVarNode?.title) + (varName) + type).length - const maxNodeNameWidth = Math.ceil(outputVarNode?.title.length / totalTextLength * availableWidth) - const maxVarNameWidth = Math.ceil(varName.length / totalTextLength * availableWidth) - const maxTypeWidth = Math.ceil(type.length / totalTextLength * availableWidth) + const totalTextLength = ((outputVarNode?.title || '') + (varName || '') + (type || '')).length + const maxNodeNameWidth = Math.floor((outputVarNode?.title?.length || 0) / totalTextLength * availableWidth) + const maxVarNameWidth = Math.floor((varName?.length || 0) / totalTextLength * availableWidth) + const maxTypeWidth = Math.floor((type?.length || 0) / totalTextLength * availableWidth) return [maxNodeNameWidth, maxVarNameWidth, maxTypeWidth] })() diff --git a/web/app/components/workflow/nodes/start/components/var-item.tsx b/web/app/components/workflow/nodes/start/components/var-item.tsx index b70d9bdd12..af2e74bba6 100644 --- a/web/app/components/workflow/nodes/start/components/var-item.tsx +++ b/web/app/components/workflow/nodes/start/components/var-item.tsx @@ -8,12 +8,13 @@ import type { InputVar } from '@/app/components/workflow/types' import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development' import { Edit03 } from '@/app/components/base/icons/src/vender/solid/general' import { Trash03 } from '@/app/components/base/icons/src/vender/line/general' +import type { MoreInfo } from '@/app/components/app/configuration/config-var/config-modal' import ConfigVarModal from '@/app/components/app/configuration/config-var/config-modal' type Props = { readonly: boolean payload: InputVar - onChange: (item: InputVar) => void + onChange: (item: InputVar, moreInfo?: MoreInfo) => void onRemove: () => void } @@ -32,8 +33,8 @@ const VarItem: FC = ({ setFalse: hideEditVarModal, }] = useBoolean(false) - const handlePayloadChange = useCallback((payload: InputVar) => { - onChange(payload) + const handlePayloadChange = useCallback((payload: InputVar, moreInfo?: MoreInfo) => { + onChange(payload, moreInfo) hideEditVarModal() }, [onChange, hideEditVarModal]) return ( diff --git a/web/app/components/workflow/nodes/start/components/var-list.tsx b/web/app/components/workflow/nodes/start/components/var-list.tsx index f5cf044a70..be33b0bbae 100644 --- a/web/app/components/workflow/nodes/start/components/var-list.tsx +++ b/web/app/components/workflow/nodes/start/components/var-list.tsx @@ -5,11 +5,11 @@ import produce from 'immer' import { useTranslation } from 'react-i18next' import VarItem from './var-item' import type { InputVar } from '@/app/components/workflow/types' - +import type { MoreInfo } from '@/app/components/app/configuration/config-var/config-modal' type Props = { readonly: boolean list: InputVar[] - onChange: (list: InputVar[]) => void + onChange: (list: InputVar[], moreInfo?: { index: number; payload: MoreInfo }) => void } const VarList: FC = ({ @@ -19,12 +19,12 @@ const VarList: FC = ({ }) => { const { t } = useTranslation() - const handleVarNameChange = useCallback((index: number) => { - return (payload: InputVar) => { + const handleVarChange = useCallback((index: number) => { + return (payload: InputVar, moreInfo?: MoreInfo) => { const newList = produce(list, (draft) => { draft[index] = payload }) - onChange(newList) + onChange(newList, moreInfo ? { index, payload: moreInfo } : undefined) } }, [list, onChange]) @@ -52,7 +52,7 @@ const VarList: FC = ({ key={index} readonly={readonly} payload={item} - onChange={handleVarNameChange(index)} + onChange={handleVarChange(index)} onRemove={handleVarRemove(index)} /> ))} diff --git a/web/app/components/workflow/nodes/start/use-config.ts b/web/app/components/workflow/nodes/start/use-config.ts index b793892519..b118906127 100644 --- a/web/app/components/workflow/nodes/start/use-config.ts +++ b/web/app/components/workflow/nodes/start/use-config.ts @@ -7,10 +7,13 @@ import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-cr import { useIsChatMode, useNodesReadOnly, + useWorkflow, } from '@/app/components/workflow/hooks' +import type { MoreInfo } from '@/app/components/app/configuration/config-var/config-modal' const useConfig = (id: string, payload: StartNodeType) => { const { nodesReadOnly: readOnly } = useNodesReadOnly() + const { handleOutVarRenameChange } = useWorkflow() const isChatMode = useIsChatMode() const { inputs, setInputs } = useNodeCrud(id, payload) @@ -20,11 +23,15 @@ const useConfig = (id: string, payload: StartNodeType) => { setFalse: hideAddVarModal, }] = useBoolean(false) - const handleVarListChange = useCallback((newList: InputVar[]) => { + const handleVarListChange = useCallback((newList: InputVar[], moreInfo?: { index: number; payload: MoreInfo }) => { const newInputs = produce(inputs, (draft: any) => { draft.variables = newList }) setInputs(newInputs) + if (moreInfo) { + const changedVar = newList[moreInfo.index] + handleOutVarRenameChange(id, [id, inputs.variables[moreInfo.index].variable], [id, changedVar.variable]) + } }, [inputs, setInputs]) const handleAddVariable = useCallback((payload: InputVar) => {