diff --git a/packages/global/core/workflow/constants.ts b/packages/global/core/workflow/constants.ts index 7832142ce..c12741b08 100644 --- a/packages/global/core/workflow/constants.ts +++ b/packages/global/core/workflow/constants.ts @@ -141,8 +141,7 @@ export enum NodeOutputKeyEnum { // plugin pluginStart = 'pluginStart', - if = 'IF', - else = 'ELSE' + ifElseResult = 'ifElseResult' } export enum VariableInputEnum { diff --git a/packages/global/core/workflow/runtime/type.d.ts b/packages/global/core/workflow/runtime/type.d.ts index af454776c..4b8fea8c9 100644 --- a/packages/global/core/workflow/runtime/type.d.ts +++ b/packages/global/core/workflow/runtime/type.d.ts @@ -75,7 +75,7 @@ export type DispatchNodeResponseType = { pluginDetail?: ChatHistoryItemResType[]; // if-else - ifElseResult?: 'IF' | 'ELSE'; + ifElseResult?: string; // tool toolCallTokens?: number; diff --git a/packages/global/core/workflow/template/system/ifElse/index.ts b/packages/global/core/workflow/template/system/ifElse/index.ts index aedc4c32c..f030e4abe 100644 --- a/packages/global/core/workflow/template/system/ifElse/index.ts +++ b/packages/global/core/workflow/template/system/ifElse/index.ts @@ -23,14 +23,6 @@ export const IfElseNode: FlowNodeTemplateType = { intro: '根据一定的条件,执行不同的分支。', showStatus: true, inputs: [ - { - key: NodeInputKeyEnum.condition, - valueType: WorkflowIOValueTypeEnum.string, - label: '', - renderTypeList: [FlowNodeInputTypeEnum.hidden], - required: false, - value: 'AND' // AND, OR - }, { key: NodeInputKeyEnum.ifElseList, renderTypeList: [FlowNodeInputTypeEnum.hidden], @@ -38,27 +30,25 @@ export const IfElseNode: FlowNodeTemplateType = { label: '', value: [ { - variable: undefined, - condition: undefined, - value: undefined + condition: 'AND', // AND, OR + list: [ + { + variable: undefined, + condition: undefined, + value: undefined + } + ] } ] } ], outputs: [ { - id: NodeOutputKeyEnum.if, - key: NodeOutputKeyEnum.if, - label: 'IF', - valueType: WorkflowIOValueTypeEnum.any, - type: FlowNodeOutputTypeEnum.source - }, - { - id: NodeOutputKeyEnum.else, - key: NodeOutputKeyEnum.else, - label: 'ELSE', - valueType: WorkflowIOValueTypeEnum.any, - type: FlowNodeOutputTypeEnum.source + id: NodeOutputKeyEnum.ifElseResult, + key: NodeOutputKeyEnum.ifElseResult, + label: 'IF ELSE', + valueType: WorkflowIOValueTypeEnum.string, + type: FlowNodeOutputTypeEnum.static } ] }; diff --git a/packages/global/core/workflow/template/system/ifElse/type.d.ts b/packages/global/core/workflow/template/system/ifElse/type.d.ts index d7997cc7b..1f55adcaa 100644 --- a/packages/global/core/workflow/template/system/ifElse/type.d.ts +++ b/packages/global/core/workflow/template/system/ifElse/type.d.ts @@ -2,8 +2,12 @@ import { ReferenceValueProps } from 'core/workflow/type/io'; import { VariableConditionEnum } from './constant'; export type IfElseConditionType = 'AND' | 'OR'; -export type IfElseListItemType = { +export type ConditionListItemType = { variable?: ReferenceValueProps; condition?: VariableConditionEnum; value?: string; }; +export type IfElseListItemType = { + condition: IfElseConditionType; + list: ConditionListItemType[]; +}; diff --git a/packages/service/core/workflow/dispatch/tools/runIfElse.ts b/packages/service/core/workflow/dispatch/tools/runIfElse.ts index edcf27ac5..ffa6d8480 100644 --- a/packages/service/core/workflow/dispatch/tools/runIfElse.ts +++ b/packages/service/core/workflow/dispatch/tools/runIfElse.ts @@ -3,6 +3,7 @@ import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runti import { DispatchNodeResultType } from '@fastgpt/global/core/workflow/runtime/type'; import { VariableConditionEnum } from '@fastgpt/global/core/workflow/template/system/ifElse/constant'; import { + ConditionListItemType, IfElseConditionType, IfElseListItemType } from '@fastgpt/global/core/workflow/template/system/ifElse/type'; @@ -41,15 +42,13 @@ function checkCondition(condition: VariableConditionEnum, variableValue: any, va return (operations[condition] || (() => false))(); } -export const dispatchIfElse = async (props: Props): Promise> => { - const { - params, - runtimeNodes, - variables, - node: { nodeId } - } = props; - const { condition, ifElseList } = params; - const listResult = ifElseList.map((item) => { +function getResult( + condition: IfElseConditionType, + list: ConditionListItemType[], + variables: any, + runtimeNodes: any[] +) { + const listResult = list.map((item) => { const { variable, condition: variableCondition, value } = item; const variableValue = getReferenceVariableValue({ @@ -61,15 +60,40 @@ export const dispatchIfElse = async (props: Props): Promise> => { + const { + params, + runtimeNodes, + variables, + node: { nodeId } + } = props; + const { ifElseList } = params; + + let res = 'ELSE'; + for (let i = 0; i < ifElseList.length; i++) { + const item = ifElseList[i]; + const result = getResult(item.condition, item.list, variables, runtimeNodes); + if (result) { + res = `IF${i}`; + break; + } + } + + const resArray = Array.from({ length: ifElseList.length + 1 }, (_, index) => { + const label = index < ifElseList.length ? `IF${index}` : 'ELSE'; + return getHandleId(nodeId, 'source', label); + }); return { [DispatchNodeResponseKeyEnum.nodeResponse]: { totalPoints: 0, - ifElseResult: result ? 'IF' : 'ELSE' + ifElseResult: res }, - [DispatchNodeResponseKeyEnum.skipHandleId]: result - ? [getHandleId(nodeId, 'source', 'ELSE')] - : [getHandleId(nodeId, 'source', 'IF')] + [DispatchNodeResponseKeyEnum.skipHandleId]: resArray.filter( + (item) => item !== getHandleId(nodeId, 'source', res) + ) }; }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c570f1da2..29ebbd096 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,4 +1,4 @@ -lockfileVersion: '6.1' +lockfileVersion: '6.0' settings: autoInstallPeers: true @@ -431,6 +431,9 @@ importers: react: specifier: 18.2.0 version: 18.2.0 + react-beautiful-dnd: + specifier: ^13.1.1 + version: 13.1.1(react-dom@18.2.0)(react@18.2.0) react-day-picker: specifier: ^8.7.1 version: 8.7.1(date-fns@2.30.0)(react@18.2.0) @@ -501,6 +504,9 @@ importers: '@types/react': specifier: 18.2.0 version: 18.2.0 + '@types/react-beautiful-dnd': + specifier: ^13.1.8 + version: 13.1.8 '@types/react-dom': specifier: 18.2.0 version: 18.2.0 @@ -3065,6 +3071,7 @@ packages: /@emotion/memoize@0.7.4: resolution: {integrity: sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==} + requiresBuild: true dev: false optional: true @@ -3682,6 +3689,7 @@ packages: /@mapbox/node-pre-gyp@1.0.11(encoding@0.1.13): resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==} hasBin: true + requiresBuild: true dependencies: detect-libc: 2.0.3 https-proxy-agent: 5.0.1 @@ -4741,12 +4749,27 @@ packages: resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} dev: true + /@types/react-beautiful-dnd@13.1.8: + resolution: {integrity: sha512-E3TyFsro9pQuK4r8S/OL6G99eq7p8v29sX0PM7oT8Z+PJfZvSQTx4zTQbUJ+QZXioAF0e7TGBEcA1XhYhCweyQ==} + dependencies: + '@types/react': 18.2.0 + dev: true + /@types/react-dom@18.2.0: resolution: {integrity: sha512-8yQrvS6sMpSwIovhPOwfyNf2Wz6v/B62LFSVYQ85+Rq3tLsBIG7rP5geMxaijTUxSkrO6RzN/IRuIAADYQsleA==} dependencies: '@types/react': 18.2.0 dev: true + /@types/react-redux@7.1.33: + resolution: {integrity: sha512-NF8m5AjWCkert+fosDsN3hAlHzpjSiXlVy9EgQEmLoBhaNXbmyeGs/aj5dQzKuF+/q+S7JQagorGDW8pJ28Hmg==} + dependencies: + '@types/hoist-non-react-statics': 3.3.5 + '@types/react': 18.2.0 + hoist-non-react-statics: 3.3.2 + redux: 4.2.1 + dev: false + /@types/react-syntax-highlighter@15.5.6: resolution: {integrity: sha512-i7wFuLbIAFlabTeD2I1cLjEOrG/xdMa/rpx2zwzAoGHuXJDhSqp9BSfDlMHSh9JSuNfxHk9eEmMX6D55GiyjGg==} dependencies: @@ -5005,6 +5028,7 @@ packages: /abbrev@1.1.1: resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + requiresBuild: true dev: false optional: true @@ -5047,6 +5071,7 @@ packages: /agent-base@6.0.2: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} + requiresBuild: true dependencies: debug: 4.3.4 transitivePeerDependencies: @@ -5170,12 +5195,14 @@ packages: /aproba@2.0.0: resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==} + requiresBuild: true dev: false optional: true /are-we-there-yet@2.0.0: resolution: {integrity: sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==} engines: {node: '>=10'} + requiresBuild: true dependencies: delegates: 1.0.0 readable-stream: 3.6.2 @@ -5791,6 +5818,7 @@ packages: /chownr@2.0.0: resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} engines: {node: '>=10'} + requiresBuild: true dev: false optional: true @@ -5903,6 +5931,7 @@ packages: /color-support@1.1.3: resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} hasBin: true + requiresBuild: true dev: false optional: true @@ -5975,6 +6004,7 @@ packages: /console-control-strings@1.1.0: resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} + requiresBuild: true dev: false optional: true @@ -6540,6 +6570,7 @@ packages: /decompress-response@4.2.1: resolution: {integrity: sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==} engines: {node: '>=8'} + requiresBuild: true dependencies: mimic-response: 2.1.0 dev: false @@ -6644,6 +6675,7 @@ packages: /delegates@1.0.0: resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} + requiresBuild: true dev: false optional: true @@ -6671,6 +6703,7 @@ packages: /detect-libc@2.0.3: resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} engines: {node: '>=8'} + requiresBuild: true dev: false optional: true @@ -7877,6 +7910,7 @@ packages: /fs-minipass@2.1.0: resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} engines: {node: '>= 8'} + requiresBuild: true dependencies: minipass: 3.3.6 dev: false @@ -7912,6 +7946,7 @@ packages: /gauge@3.0.2: resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==} engines: {node: '>=10'} + requiresBuild: true dependencies: aproba: 2.0.0 color-support: 1.1.3 @@ -8089,6 +8124,7 @@ packages: /has-unicode@2.0.1: resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} + requiresBuild: true dev: false optional: true @@ -8246,6 +8282,7 @@ packages: /https-proxy-agent@5.0.1: resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} engines: {node: '>= 6'} + requiresBuild: true dependencies: agent-base: 6.0.2 debug: 4.3.4 @@ -9088,6 +9125,7 @@ packages: /make-dir@3.1.0: resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} engines: {node: '>=8'} + requiresBuild: true dependencies: semver: 6.3.1 dev: false @@ -9289,8 +9327,13 @@ packages: engines: {node: '>= 0.6'} dev: false + /memoize-one@5.2.1: + resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} + dev: false + /memory-pager@1.5.0: resolution: {integrity: sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==} + requiresBuild: true dev: false optional: true @@ -9647,6 +9690,7 @@ packages: /mimic-response@2.1.0: resolution: {integrity: sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==} engines: {node: '>=8'} + requiresBuild: true dev: false optional: true @@ -9669,6 +9713,7 @@ packages: /minipass@3.3.6: resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} engines: {node: '>=8'} + requiresBuild: true dependencies: yallist: 4.0.0 dev: false @@ -9677,12 +9722,14 @@ packages: /minipass@5.0.0: resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} engines: {node: '>=8'} + requiresBuild: true dev: false optional: true /minizlib@2.1.2: resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} engines: {node: '>= 8'} + requiresBuild: true dependencies: minipass: 3.3.6 yallist: 4.0.0 @@ -9700,6 +9747,7 @@ packages: resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} engines: {node: '>=10'} hasBin: true + requiresBuild: true dev: false optional: true @@ -9981,6 +10029,7 @@ packages: resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==} engines: {node: '>=6'} hasBin: true + requiresBuild: true dependencies: abbrev: 1.1.1 dev: false @@ -9999,6 +10048,7 @@ packages: /npmlog@5.0.1: resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==} + requiresBuild: true dependencies: are-we-there-yet: 2.0.0 console-control-strings: 1.1.0 @@ -10309,6 +10359,7 @@ packages: /path2d@0.1.1: resolution: {integrity: sha512-/+S03c8AGsDYKKBtRDqieTJv2GlkMb0bWjnqOgtF6MkjdUQ9a8ARAtxWf9NgKLGm2+WQr6+/tqJdU8HNGsIDoA==} engines: {node: '>=6'} + requiresBuild: true dev: false optional: true @@ -10587,6 +10638,10 @@ packages: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} dev: true + /raf-schd@4.0.3: + resolution: {integrity: sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==} + dev: false + /randombytes@2.1.0: resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} dependencies: @@ -10614,6 +10669,25 @@ packages: unpipe: 1.0.0 dev: false + /react-beautiful-dnd@13.1.1(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==} + peerDependencies: + react: ^16.8.5 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.5 || ^17.0.0 || ^18.0.0 + dependencies: + '@babel/runtime': 7.24.1 + css-box-model: 1.2.1 + memoize-one: 5.2.1 + raf-schd: 4.0.3 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-redux: 7.2.9(react-dom@18.2.0)(react@18.2.0) + redux: 4.2.1 + use-memo-one: 1.1.3(react@18.2.0) + transitivePeerDependencies: + - react-native + dev: false + /react-clientside-effect@1.2.6(react@18.2.0): resolution: {integrity: sha512-XGGGRQAKY+q25Lz9a/4EPqom7WRjz3z9R2k4jhVKA/puQFH/5Nt27vFZYql4m4NVNdUvX8PS3O7r/Zzm7cjUlg==} peerDependencies: @@ -10706,6 +10780,10 @@ packages: /react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + /react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + dev: false + /react-is@18.2.0: resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} dev: false @@ -10737,6 +10815,28 @@ packages: - supports-color dev: false + /react-redux@7.2.9(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==} + peerDependencies: + react: ^16.8.3 || ^17 || ^18 + react-dom: '*' + react-native: '*' + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + dependencies: + '@babel/runtime': 7.24.1 + '@types/react-redux': 7.1.33 + hoist-non-react-statics: 3.3.2 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-is: 17.0.2 + dev: false + /react-remove-scroll-bar@2.3.6(@types/react@18.2.0)(react@18.2.0): resolution: {integrity: sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==} engines: {node: '>=10'} @@ -10858,6 +10958,12 @@ packages: dependencies: picomatch: 2.3.1 + /redux@4.2.1: + resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==} + dependencies: + '@babel/runtime': 7.24.1 + dev: false + /reflect.getprototypeof@1.0.6: resolution: {integrity: sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==} engines: {node: '>= 0.4'} @@ -11262,6 +11368,7 @@ packages: /set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + requiresBuild: true dev: false optional: true @@ -11331,11 +11438,13 @@ packages: /simple-concat@1.0.1: resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + requiresBuild: true dev: false optional: true /simple-get@3.1.1: resolution: {integrity: sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==} + requiresBuild: true dependencies: decompress-response: 4.2.1 once: 1.4.0 @@ -11417,6 +11526,7 @@ packages: /sparse-bitfield@3.0.3: resolution: {integrity: sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==} + requiresBuild: true dependencies: memory-pager: 1.5.0 dev: false @@ -11685,6 +11795,7 @@ packages: /tar@6.2.1: resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} engines: {node: '>=10'} + requiresBuild: true dependencies: chownr: 2.0.0 fs-minipass: 2.1.0 @@ -12182,6 +12293,14 @@ packages: scheduler: 0.23.0 dev: false + /use-memo-one@1.1.3(react@18.2.0): + resolution: {integrity: sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + react: 18.2.0 + dev: false + /use-sidecar@1.1.2(@types/react@18.2.0)(react@18.2.0): resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==} engines: {node: '>=10'} @@ -12539,6 +12658,7 @@ packages: /wide-align@1.1.5: resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} + requiresBuild: true dependencies: string-width: 4.2.3 dev: false diff --git a/projects/app/i18n/en/common.json b/projects/app/i18n/en/common.json index 50c62a0b3..96d435554 100644 --- a/projects/app/i18n/en/common.json +++ b/projects/app/i18n/en/common.json @@ -935,6 +935,7 @@ }, "input": { "Add Input": "Add Input", + "Add Branch": "Add Branch", "Input Number": "Input: {{length}}", "add": "", "description": { diff --git a/projects/app/i18n/zh/common.json b/projects/app/i18n/zh/common.json index 9a18c2418..7069ba9f2 100644 --- a/projects/app/i18n/zh/common.json +++ b/projects/app/i18n/zh/common.json @@ -937,6 +937,7 @@ "Add Input": "添加入参", "Input Number": "入参: {{length}}", "add": "添加条件", + "Add Branch": "添加分支", "description": { "Background": "你可以添加一些特定内容的介绍,从而更好的识别用户的问题类型。这个内容通常是给模型介绍一个它不知道的内容。", "HTTP Dynamic Input": "接收前方节点的输出值作为变量,这些变量可以被HTTP请求参数使用。", diff --git a/projects/app/package.json b/projects/app/package.json index 6975d9fa1..0667849c0 100644 --- a/projects/app/package.json +++ b/projects/app/package.json @@ -47,6 +47,7 @@ "nextjs-node-loader": "^1.1.5", "nprogress": "^0.2.0", "react": "18.2.0", + "react-beautiful-dnd": "^13.1.1", "react-day-picker": "^8.7.1", "react-dom": "18.2.0", "react-hook-form": "7.43.1", @@ -72,6 +73,7 @@ "@types/lodash": "^4.14.191", "@types/node": "^20.8.5", "@types/react": "18.2.0", + "@types/react-beautiful-dnd": "^13.1.8", "@types/react-dom": "18.2.0", "@types/react-syntax-highlighter": "^15.5.6", "@types/request-ip": "^0.0.37", diff --git a/projects/app/src/components/core/workflow/Flow/nodes/NodeIfElse.tsx b/projects/app/src/components/core/workflow/Flow/nodes/NodeIfElse.tsx deleted file mode 100644 index e8649d354..000000000 --- a/projects/app/src/components/core/workflow/Flow/nodes/NodeIfElse.tsx +++ /dev/null @@ -1,381 +0,0 @@ -import React, { useCallback, useMemo } from 'react'; -import NodeCard from './render/NodeCard'; -import { useTranslation } from 'next-i18next'; -import { Box, Button, Flex, background } from '@chakra-ui/react'; -import { SmallAddIcon } from '@chakra-ui/icons'; -import MyIcon from '@fastgpt/web/components/common/Icon'; -import RenderOutput from './render/RenderOutput'; -import { NodeInputKeyEnum, WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants'; -import { NodeProps } from 'reactflow'; -import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type'; -import { - IfElseConditionType, - IfElseListItemType -} from '@fastgpt/global/core/workflow/template/system/ifElse/type'; -import { ReferenceValueProps } from '@fastgpt/global/core/workflow/type/io'; -import { ReferSelector, useReference } from './render/RenderInput/templates/Reference'; -import { - VariableConditionEnum, - allConditionList, - arrayConditionList, - booleanConditionList, - numberConditionList -} from '@fastgpt/global/core/workflow/template/system/ifElse/constant'; -import { stringConditionList } from '@fastgpt/global/core/workflow/template/system/ifElse/constant'; -import MySelect from '@fastgpt/web/components/common/MySelect'; -import MyInput from '@/components/MyInput'; -import { useContextSelector } from 'use-context-selector'; -import { WorkflowContext } from '../../context'; - -const NodeIfElse = ({ data, selected }: NodeProps) => { - const { t } = useTranslation(); - const { nodeId, inputs = [], outputs } = data; - const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode); - - const condition = useMemo( - () => - (inputs.find((input) => input.key === NodeInputKeyEnum.condition) - ?.value as IfElseConditionType) || 'OR', - [inputs] - ); - const ifElseList = useMemo( - () => - (inputs.find((input) => input.key === NodeInputKeyEnum.ifElseList) - ?.value as IfElseListItemType[]) || [], - [inputs] - ); - - const onUpdateIfElseList = useCallback( - (value: IfElseListItemType[]) => { - const ifElseListInput = inputs.find((input) => input.key === NodeInputKeyEnum.ifElseList); - if (!ifElseListInput) return; - - onChangeNode({ - nodeId, - type: 'updateInput', - key: NodeInputKeyEnum.ifElseList, - value: { - ...ifElseListInput, - value - } - }); - }, - [inputs, nodeId, onChangeNode] - ); - - const RenderAddCondition = useMemo(() => { - return ( - - ); - }, [ifElseList, onUpdateIfElseList, t]); - - return ( - - - - - - - {ifElseList.map((item, i) => { - return ( - - {/* border */} - {i !== 0 && ( - - - { - const conditionInput = inputs.find( - (input) => input.key === NodeInputKeyEnum.condition - ); - if (!conditionInput) return; - - onChangeNode({ - nodeId, - type: 'updateInput', - key: NodeInputKeyEnum.condition, - value: { - ...conditionInput, - value: conditionInput.value === 'OR' ? 'AND' : 'OR' - } - }); - }} - > - {condition} - - - - - )} - {/* condition list */} - - {/* variable reference */} - - { - onUpdateIfElseList( - ifElseList.map((ifElse, index) => { - if (index === i) { - return { - ...ifElse, - variable: e - }; - } - return ifElse; - }) - ); - }} - /> - - {/* condition select */} - - { - onUpdateIfElseList( - ifElseList.map((ifElse, index) => { - if (index === i) { - return { - ...ifElse, - condition: e - }; - } - return ifElse; - }) - ); - }} - /> - - {/* value */} - - { - onUpdateIfElseList( - ifElseList.map((ifElse, index) => { - if (index === i) { - return { - ...ifElse, - value: e - }; - } - return ifElse; - }) - ); - }} - /> - - {/* delete */} - {ifElseList.length > 1 && ( - { - onUpdateIfElseList(ifElseList.filter((_, index) => index !== i)); - }} - /> - )} - - - ); - })} - - {RenderAddCondition} - - - - - - ); -}; -export default React.memo(NodeIfElse); - -const Reference = ({ - nodeId, - variable, - onSelect -}: { - nodeId: string; - variable?: ReferenceValueProps; - onSelect: (e: ReferenceValueProps) => void; -}) => { - const { t } = useTranslation(); - - const { referenceList, formatValue } = useReference({ - nodeId, - valueType: WorkflowIOValueTypeEnum.any, - value: variable - }); - - return ( - - ); -}; - -/* Different data types have different options */ -const ConditionSelect = ({ - condition, - variable, - onSelect -}: { - condition?: VariableConditionEnum; - variable?: ReferenceValueProps; - onSelect: (e: VariableConditionEnum) => void; -}) => { - const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList); - - // get condition type - const valueType = useMemo(() => { - if (!variable) return; - const node = nodeList.find((node) => node.nodeId === variable[0]); - - if (!node) return WorkflowIOValueTypeEnum.any; - const output = node.outputs.find((item) => item.id === variable[1]); - - if (!output) return WorkflowIOValueTypeEnum.any; - return output.valueType; - }, [nodeList, variable]); - - const conditionList = useMemo(() => { - if (valueType === WorkflowIOValueTypeEnum.string) return stringConditionList; - if (valueType === WorkflowIOValueTypeEnum.number) return numberConditionList; - if (valueType === WorkflowIOValueTypeEnum.boolean) return booleanConditionList; - if ( - valueType === WorkflowIOValueTypeEnum.chatHistory || - valueType === WorkflowIOValueTypeEnum.datasetQuote || - valueType === WorkflowIOValueTypeEnum.dynamic || - valueType === WorkflowIOValueTypeEnum.selectApp || - valueType === WorkflowIOValueTypeEnum.arrayBoolean || - valueType === WorkflowIOValueTypeEnum.arrayNumber || - valueType === WorkflowIOValueTypeEnum.arrayObject || - valueType === WorkflowIOValueTypeEnum.arrayString || - valueType === WorkflowIOValueTypeEnum.object - ) - return arrayConditionList; - - if (valueType === WorkflowIOValueTypeEnum.any) return allConditionList; - - return []; - }, [valueType]); - - return ( - - ); -}; - -/* - Different condition can be entered differently - empty, notEmpty: forbid input - boolean type: select true/false -*/ -const ConditionValueInput = ({ - value = '', - variable, - condition, - onChange -}: { - value?: string; - variable?: ReferenceValueProps; - condition?: VariableConditionEnum; - onChange: (e: string) => void; -}) => { - const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList); - - // get value type - const valueType = useMemo(() => { - if (!variable) return; - const node = nodeList.find((node) => node.nodeId === variable[0]); - - if (!node) return WorkflowIOValueTypeEnum.any; - const output = node.outputs.find((item) => item.id === variable[1]); - - if (!output) return WorkflowIOValueTypeEnum.any; - return output.valueType; - }, [nodeList, variable]); - - if (valueType === WorkflowIOValueTypeEnum.boolean) { - return ( - - ); - } else { - return ( - onChange(e.target.value)} - /> - ); - } -}; diff --git a/projects/app/src/components/core/workflow/Flow/nodes/NodeIfElse/ListItem.tsx b/projects/app/src/components/core/workflow/Flow/nodes/NodeIfElse/ListItem.tsx new file mode 100644 index 000000000..b00bb757b --- /dev/null +++ b/projects/app/src/components/core/workflow/Flow/nodes/NodeIfElse/ListItem.tsx @@ -0,0 +1,412 @@ +import { Box, Button, Flex } from '@chakra-ui/react'; +import { DraggableProvided, DraggableStateSnapshot } from 'react-beautiful-dnd'; +import Container from '../../components/Container'; +import { DragHandleIcon, MinusIcon, SmallAddIcon } from '@chakra-ui/icons'; +import { IfElseListItemType } from '@fastgpt/global/core/workflow/template/system/ifElse/type'; +import MyIcon from '@fastgpt/web/components/common/Icon'; +import { ReferenceValueProps } from '@fastgpt/global/core/workflow/type/io'; +import { useTranslation } from 'react-i18next'; +import { ReferSelector, useReference } from '../render/RenderInput/templates/Reference'; +import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants'; +import { + VariableConditionEnum, + allConditionList, + arrayConditionList, + booleanConditionList, + numberConditionList, + stringConditionList +} from '@fastgpt/global/core/workflow/template/system/ifElse/constant'; +import { useContextSelector } from 'use-context-selector'; +import React, { useMemo } from 'react'; +import { WorkflowContext } from '../../../context'; +import MySelect from '@fastgpt/web/components/common/MySelect'; +import MyInput from '@/components/MyInput'; +import { getHandleId } from '@fastgpt/global/core/workflow/utils'; +import { SourceHandle } from '../render/Handle'; +import { Position, useReactFlow } from 'reactflow'; + +const ListItem = ({ + provided, + snapshot, + conditionIndex, + conditionItem, + ifElseList, + onUpdateIfElseList, + nodeId +}: { + provided: DraggableProvided; + snapshot: DraggableStateSnapshot; + conditionIndex: number; + conditionItem: IfElseListItemType; + ifElseList: IfElseListItemType[]; + onUpdateIfElseList: (value: IfElseListItemType[]) => void; + nodeId: string; +}) => { + const { t } = useTranslation(); + const { getZoom } = useReactFlow(); + + return ( + + + + + + + + + {conditionIndex === 0 ? 'IF' : 'ELSE IF'} + + { + onUpdateIfElseList( + ifElseList.map((ifElse, index) => { + if (index === conditionIndex) { + return { + ...ifElse, + condition: ifElse.condition === 'AND' ? 'OR' : 'AND' + }; + } + return ifElse; + }) + ); + }} + > + {conditionItem.condition} + + + + {ifElseList.length > 1 && ( + { + onUpdateIfElseList(ifElseList.filter((_, index) => index !== conditionIndex)); + }} + /> + )} + + + {conditionItem.list?.map((item, i) => { + return ( + + {/* condition list */} + + {/* variable reference */} + + { + onUpdateIfElseList( + ifElseList.map((ifElse, index) => { + if (index === conditionIndex) { + return { + ...ifElse, + list: ifElse.list.map((item, index) => { + if (index === i) { + return { + ...item, + variable: e + }; + } + return item; + }) + }; + } + return ifElse; + }) + ); + }} + /> + + {/* condition select */} + + { + onUpdateIfElseList( + ifElseList.map((ifElse, index) => { + if (index === conditionIndex) { + return { + ...ifElse, + list: ifElse.list.map((item, index) => { + if (index === i) { + return { + ...item, + condition: e + }; + } + return item; + }) + }; + } + return ifElse; + }) + ); + }} + /> + + {/* value */} + + { + onUpdateIfElseList( + ifElseList.map((ifElse, index) => { + if (index === conditionIndex) { + return { + ...ifElse, + list: ifElse.list.map((item, index) => { + if (index === i) { + return { + ...item, + value: e + }; + } + return item; + }) + }; + } + return ifElse; + }) + ); + }} + /> + + {/* delete */} + {conditionItem.list.length > 1 && ( + { + onUpdateIfElseList( + ifElseList.map((ifElse, index) => { + if (index === conditionIndex) { + return { + ...ifElse, + list: ifElse.list.filter((_, index) => index !== i) + }; + } + return ifElse; + }) + ); + }} + /> + )} + + + ); + })} + + + + {!snapshot.isDragging && ( + + )} + + + ); +}; + +export default React.memo(ListItem); + +const Reference = ({ + nodeId, + variable, + onSelect +}: { + nodeId: string; + variable?: ReferenceValueProps; + onSelect: (e: ReferenceValueProps) => void; +}) => { + const { t } = useTranslation(); + + const { referenceList, formatValue } = useReference({ + nodeId, + valueType: WorkflowIOValueTypeEnum.any, + value: variable + }); + + return ( + + ); +}; + +/* Different data types have different options */ +const ConditionSelect = ({ + condition, + variable, + onSelect +}: { + condition?: VariableConditionEnum; + variable?: ReferenceValueProps; + onSelect: (e: VariableConditionEnum) => void; +}) => { + const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList); + + // get condition type + const valueType = useMemo(() => { + if (!variable) return; + const node = nodeList.find((node) => node.nodeId === variable[0]); + + if (!node) return WorkflowIOValueTypeEnum.any; + const output = node.outputs.find((item) => item.id === variable[1]); + + if (!output) return WorkflowIOValueTypeEnum.any; + return output.valueType; + }, [nodeList, variable]); + + const conditionList = useMemo(() => { + if (valueType === WorkflowIOValueTypeEnum.string) return stringConditionList; + if (valueType === WorkflowIOValueTypeEnum.number) return numberConditionList; + if (valueType === WorkflowIOValueTypeEnum.boolean) return booleanConditionList; + if ( + valueType === WorkflowIOValueTypeEnum.chatHistory || + valueType === WorkflowIOValueTypeEnum.datasetQuote || + valueType === WorkflowIOValueTypeEnum.dynamic || + valueType === WorkflowIOValueTypeEnum.selectApp || + valueType === WorkflowIOValueTypeEnum.arrayBoolean || + valueType === WorkflowIOValueTypeEnum.arrayNumber || + valueType === WorkflowIOValueTypeEnum.arrayObject || + valueType === WorkflowIOValueTypeEnum.arrayString || + valueType === WorkflowIOValueTypeEnum.object + ) + return arrayConditionList; + + if (valueType === WorkflowIOValueTypeEnum.any) return allConditionList; + + return []; + }, [valueType]); + + return ( + + ); +}; + +/* + Different condition can be entered differently + empty, notEmpty: forbid input + boolean type: select true/false +*/ +const ConditionValueInput = ({ + value = '', + variable, + condition, + onChange +}: { + value?: string; + variable?: ReferenceValueProps; + condition?: VariableConditionEnum; + onChange: (e: string) => void; +}) => { + const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList); + + // get value type + const valueType = useMemo(() => { + if (!variable) return; + const node = nodeList.find((node) => node.nodeId === variable[0]); + + if (!node) return WorkflowIOValueTypeEnum.any; + const output = node.outputs.find((item) => item.id === variable[1]); + + if (!output) return WorkflowIOValueTypeEnum.any; + return output.valueType; + }, [nodeList, variable]); + + if (valueType === WorkflowIOValueTypeEnum.boolean) { + return ( + + ); + } else { + return ( + onChange(e.target.value)} + /> + ); + } +}; diff --git a/projects/app/src/components/core/workflow/Flow/nodes/NodeIfElse/index.tsx b/projects/app/src/components/core/workflow/Flow/nodes/NodeIfElse/index.tsx new file mode 100644 index 000000000..3eade31a4 --- /dev/null +++ b/projects/app/src/components/core/workflow/Flow/nodes/NodeIfElse/index.tsx @@ -0,0 +1,161 @@ +import React, { useCallback, useMemo, useState } from 'react'; +import NodeCard from '../render/NodeCard'; +import { useTranslation } from 'next-i18next'; +import { Box, Button, Flex } from '@chakra-ui/react'; +import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants'; +import { NodeProps, Position } from 'reactflow'; +import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type'; +import { IfElseListItemType } from '@fastgpt/global/core/workflow/template/system/ifElse/type'; +import { useContextSelector } from 'use-context-selector'; +import { WorkflowContext } from '../../../context'; +import Container from '../../components/Container'; +import { DragDropContext, DragStart, Draggable, DropResult, Droppable } from 'react-beautiful-dnd'; +import { SourceHandle } from '../render/Handle'; +import { getHandleId } from '@fastgpt/global/core/workflow/utils'; +import ListItem from './ListItem'; + +const NodeIfElse = ({ data, selected }: NodeProps) => { + const { t } = useTranslation(); + const { nodeId, inputs = [] } = data; + const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode); + + const [draggingItemHeight, setDraggingItemHeight] = useState(0); + + const ifElseList = useMemo( + () => + (inputs.find((input) => input.key === NodeInputKeyEnum.ifElseList) + ?.value as IfElseListItemType[]) || [], + [inputs] + ); + + const onUpdateIfElseList = useCallback( + (value: IfElseListItemType[]) => { + const ifElseListInput = inputs.find((input) => input.key === NodeInputKeyEnum.ifElseList); + if (!ifElseListInput) return; + + onChangeNode({ + nodeId, + type: 'updateInput', + key: NodeInputKeyEnum.ifElseList, + value: { + ...ifElseListInput, + value + } + }); + }, + [inputs, nodeId, onChangeNode] + ); + + const reorder = (list: IfElseListItemType[], startIndex: number, endIndex: number) => { + const result = Array.from(list); + const [removed] = result.splice(startIndex, 1); + result.splice(endIndex, 0, removed); + + return result; + }; + + const onDragStart = (start: DragStart) => { + const draggingNode = document.querySelector(`[data-rbd-draggable-id="${start.draggableId}"]`); + setDraggingItemHeight(draggingNode?.getBoundingClientRect().height || 0); + }; + + const onDragEnd = (result: DropResult) => { + if (!result.destination) { + return; + } + const newList = reorder(ifElseList, result.source.index, result.destination.index); + + onUpdateIfElseList(newList); + setDraggingItemHeight(0); + }; + + return ( + + + + ( + + )} + > + {(provided, snapshot) => ( + + {ifElseList.map((conditionItem, conditionIndex) => ( + + {(provided, snapshot) => ( + + )} + + ))} + + + )} + + + + + + ELSE + + + + + + + + + + ); +}; +export default React.memo(NodeIfElse);