diff --git a/web/app/components/base/button/index.css b/web/app/components/base/button/index.css index f3800d0375..17932166ca 100644 --- a/web/app/components/base/button/index.css +++ b/web/app/components/base/button/index.css @@ -40,4 +40,8 @@ .btn-ghost { @apply bg-transparent hover:bg-gray-200 border-transparent shadow-none text-gray-700; } + + .btn-tertiary { + @apply bg-[#F2F4F7] hover:bg-[#E9EBF0] border-transparent shadow-none text-gray-700; + } } \ No newline at end of file diff --git a/web/app/components/base/button/index.tsx b/web/app/components/base/button/index.tsx index b03105e397..959b2dbe7b 100644 --- a/web/app/components/base/button/index.tsx +++ b/web/app/components/base/button/index.tsx @@ -14,6 +14,7 @@ const buttonVariants = cva( 'secondary': 'btn-secondary', 'secondary-accent': 'btn-secondary-accent', 'ghost': 'btn-ghost', + 'tertiary': 'btn-tertiary', }, size: { small: 'btn-small', 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 56a14ec8e4..a0743ddb9f 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 @@ -88,7 +88,7 @@ const WorkflowVariableBlockComponent = ({ ) } -
{node?.title}
diff --git a/web/app/components/workflow/constants.ts b/web/app/components/workflow/constants.ts index 1786ca4b47..aa4545cefb 100644 --- a/web/app/components/workflow/constants.ts +++ b/web/app/components/workflow/constants.ts @@ -360,7 +360,7 @@ export const HTTP_REQUEST_OUTPUT_STRUCT: Var[] = [ }, { variable: 'headers', - type: VarType.string, + type: VarType.object, }, { variable: 'files', diff --git a/web/app/components/workflow/index.tsx b/web/app/components/workflow/index.tsx index a9a4b40ef3..aca8935f62 100644 --- a/web/app/components/workflow/index.tsx +++ b/web/app/components/workflow/index.tsx @@ -21,6 +21,7 @@ import ReactFlow, { useNodesState, useOnViewportChange, useReactFlow, + useStoreApi, } from 'reactflow' import type { Viewport, @@ -278,6 +279,15 @@ const Workflow: FC = memo(({ { exactMatch: true, useCapture: true }, ) + const store = useStoreApi() + if (process.env.NODE_ENV === 'development') { + store.getState().onError = (code, message) => { + if (code === '002') + return + console.warn(message) + } + } + return (
= ({ nodeId, value, + className, }) => { const { getBeforeNodesInSameBranchIncludeParent } = useWorkflow() const availableNodes = getBeforeNodesInSameBranchIncludeParent(nodeId) @@ -64,7 +67,7 @@ const ReadonlyInputWithSelectVar: FC = ({ })() return ( -
+
{res}
) diff --git a/web/app/components/workflow/nodes/_base/components/variable-tag.tsx b/web/app/components/workflow/nodes/_base/components/variable-tag.tsx new file mode 100644 index 0000000000..a13e44097f --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/variable-tag.tsx @@ -0,0 +1,66 @@ +import { useMemo } from 'react' +import { useNodes } from 'reactflow' +import { capitalize } from 'lodash-es' +import { VarBlockIcon } from '@/app/components/workflow/block-icon' +import type { + CommonNodeType, + ValueSelector, + VarType, +} from '@/app/components/workflow/types' +import { BlockEnum } from '@/app/components/workflow/types' +import { Line3 } from '@/app/components/base/icons/src/public/common' +import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development' +import { isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils' + +type VariableTagProps = { + valueSelector: ValueSelector + varType: VarType +} +const VariableTag = ({ + valueSelector, + varType, +}: VariableTagProps) => { + const nodes = useNodes() + const node = useMemo(() => { + if (isSystemVar(valueSelector)) + return nodes.find(node => node.data.type === BlockEnum.Start) + + return nodes.find(node => node.id === valueSelector[0]) + }, [nodes, valueSelector]) + + const variableName = isSystemVar(valueSelector) ? valueSelector.slice(0).join('.') : valueSelector.slice(1).join('.') + + return ( +
+ { + node && ( + + ) + } +
+ {node?.data.title} +
+ + +
+ {variableName} +
+ { + varType && ( +
{capitalize(varType)}
+ ) + } +
+ ) +} + +export default VariableTag diff --git a/web/app/components/workflow/nodes/if-else/components/condition-add.tsx b/web/app/components/workflow/nodes/if-else/components/condition-add.tsx new file mode 100644 index 0000000000..ec1851c30d --- /dev/null +++ b/web/app/components/workflow/nodes/if-else/components/condition-add.tsx @@ -0,0 +1,75 @@ +import { + useCallback, + useState, +} from 'react' +import { useTranslation } from 'react-i18next' +import { RiAddLine } from '@remixicon/react' +import type { HandleAddCondition } from '../types' +import Button from '@/app/components/base/button' +import { + PortalToFollowElem, + PortalToFollowElemContent, + PortalToFollowElemTrigger, +} from '@/app/components/base/portal-to-follow-elem' +import VarReferenceVars from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars' +import type { + NodeOutPutVar, + ValueSelector, + Var, +} from '@/app/components/workflow/types' + +type ConditionAddProps = { + className?: string + caseId: string + variables: NodeOutPutVar[] + onSelectVariable: HandleAddCondition + disabled?: boolean +} +const ConditionAdd = ({ + className, + caseId, + variables, + onSelectVariable, + disabled, +}: ConditionAddProps) => { + const { t } = useTranslation() + const [open, setOpen] = useState(false) + + const handleSelectVariable = useCallback((valueSelector: ValueSelector, varItem: Var) => { + onSelectVariable(caseId, valueSelector, varItem) + setOpen(false) + }, [caseId, onSelectVariable, setOpen]) + + return ( + + setOpen(!open)}> + + + +
+ +
+
+
+ ) +} + +export default ConditionAdd diff --git a/web/app/components/workflow/nodes/if-else/components/condition-item.tsx b/web/app/components/workflow/nodes/if-else/components/condition-item.tsx deleted file mode 100644 index d39ca7e2fb..0000000000 --- a/web/app/components/workflow/nodes/if-else/components/condition-item.tsx +++ /dev/null @@ -1,250 +0,0 @@ -'use client' -import type { FC } from 'react' -import React, { useCallback, useEffect } from 'react' -import { useTranslation } from 'react-i18next' -import { - RiDeleteBinLine, -} from '@remixicon/react' -import VarReferencePicker from '../../_base/components/variable/var-reference-picker' -import { isComparisonOperatorNeedTranslate } from '../utils' -import { VarType } from '../../../types' -import cn from '@/utils/classnames' -import type { Condition } from '@/app/components/workflow/nodes/if-else/types' -import { ComparisonOperator, LogicalOperator } from '@/app/components/workflow/nodes/if-else/types' -import type { ValueSelector, Var } from '@/app/components/workflow/types' -import { RefreshCw05 } from '@/app/components/base/icons/src/vender/line/arrows' -import Selector from '@/app/components/workflow/nodes/_base/components/selector' -import Toast from '@/app/components/base/toast' - -const i18nPrefix = 'workflow.nodes.ifElse' - -const Line = ( - - - - - - - - - -) - -const getOperators = (type?: VarType) => { - switch (type) { - case VarType.string: - return [ - ComparisonOperator.contains, - ComparisonOperator.notContains, - ComparisonOperator.startWith, - ComparisonOperator.endWith, - ComparisonOperator.is, - ComparisonOperator.isNot, - ComparisonOperator.empty, - ComparisonOperator.notEmpty, - ] - case VarType.number: - return [ - ComparisonOperator.equal, - ComparisonOperator.notEqual, - ComparisonOperator.largerThan, - ComparisonOperator.lessThan, - ComparisonOperator.largerThanOrEqual, - ComparisonOperator.lessThanOrEqual, - ComparisonOperator.is, - ComparisonOperator.isNot, - ComparisonOperator.empty, - ComparisonOperator.notEmpty, - ] - case VarType.arrayString: - case VarType.arrayNumber: - return [ - ComparisonOperator.contains, - ComparisonOperator.notContains, - ComparisonOperator.empty, - ComparisonOperator.notEmpty, - ] - case VarType.array: - case VarType.arrayObject: - return [ - ComparisonOperator.empty, - ComparisonOperator.notEmpty, - ] - default: - return [ - ComparisonOperator.is, - ComparisonOperator.isNot, - ComparisonOperator.empty, - ComparisonOperator.notEmpty, - ] - } -} - -type ItemProps = { - readonly: boolean - nodeId: string - payload: Condition - varType?: VarType - onChange: (newItem: Condition) => void - canRemove: boolean - onRemove?: () => void - isShowLogicalOperator?: boolean - logicalOperator: LogicalOperator - onLogicalOperatorToggle: () => void - filterVar: (varPayload: Var) => boolean -} - -const Item: FC = ({ - readonly, - nodeId, - payload, - varType = VarType.string, - onChange, - canRemove, - onRemove = () => { }, - isShowLogicalOperator, - logicalOperator, - onLogicalOperatorToggle, - filterVar, -}) => { - const { t } = useTranslation() - const isValueReadOnly = payload.comparison_operator ? [ComparisonOperator.empty, ComparisonOperator.notEmpty, ComparisonOperator.isNull, ComparisonOperator.isNotNull].includes(payload.comparison_operator) : false - - const handleVarReferenceChange = useCallback((value: ValueSelector | string) => { - onChange({ - ...payload, - variable_selector: value as ValueSelector, - }) - }, [onChange, payload]) - - // change to default operator if the variable type is changed - useEffect(() => { - if (varType && payload.comparison_operator) { - if (!getOperators(varType).includes(payload.comparison_operator)) { - onChange({ - ...payload, - comparison_operator: getOperators(varType)[0], - }) - } - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [varType, payload]) - - const handleValueChange = useCallback((e: React.ChangeEvent) => { - onChange({ - ...payload, - value: e.target.value, - }) - }, [onChange, payload]) - - const handleComparisonOperatorChange = useCallback((v: ComparisonOperator) => { - onChange({ - ...payload, - comparison_operator: v, - }) - }, [onChange, payload]) - - return ( -
- {isShowLogicalOperator && ( -
-
- {Line} -
-
{t(`${i18nPrefix}.${logicalOperator === LogicalOperator.and ? 'and' : 'or'}`)}
- -
-
- {Line} -
-
-
- ) - } - -
- - - { - if (readonly) { - e.stopPropagation() - return - } - if (!payload.variable_selector || payload.variable_selector.length === 0) { - e.stopPropagation() - Toast.notify({ - message: t(`${i18nPrefix}.notSetVariable`), - type: 'error', - }) - } - }} - className={cn(!readonly && 'cursor-pointer', 'shrink-0 w-[100px] whitespace-nowrap flex items-center h-8 justify-between px-2.5 rounded-lg bg-gray-100 capitalize')} - > - { - !payload.comparison_operator - ?
{t(`${i18nPrefix}.operator`)}
- :
{isComparisonOperatorNeedTranslate(payload.comparison_operator) ? t(`${i18nPrefix}.comparisonOperator.${payload.comparison_operator}`) : payload.comparison_operator}
- } - -
- } - readonly={readonly} - value={payload.comparison_operator || ''} - options={getOperators(varType).map((o) => { - return { - label: isComparisonOperatorNeedTranslate(o) ? t(`${i18nPrefix}.comparisonOperator.${o}`) : o, - value: o, - } - })} - onChange={handleComparisonOperatorChange} - /> - - { - if (readonly) - return - - if (!varType) { - Toast.notify({ - message: t(`${i18nPrefix}.notSetVariable`), - type: 'error', - }) - } - }} - value={!isValueReadOnly ? payload.value : ''} - onChange={handleValueChange} - placeholder={(!readonly && !isValueReadOnly) ? t(`${i18nPrefix}.enterValue`)! : ''} - className='min-w-[80px] flex-grow h-8 leading-8 px-2.5 rounded-lg border-0 bg-gray-100 text-gray-900 text-[13px] placeholder:text-gray-400 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200' - type='text' - /> - {!readonly && ( -
{ }} - > - -
- )} -
-
- - ) -} -export default React.memo(Item) diff --git a/web/app/components/workflow/nodes/if-else/components/condition-list.tsx b/web/app/components/workflow/nodes/if-else/components/condition-list.tsx deleted file mode 100644 index f6302b9811..0000000000 --- a/web/app/components/workflow/nodes/if-else/components/condition-list.tsx +++ /dev/null @@ -1,91 +0,0 @@ -'use client' -import type { FC } from 'react' -import React, { useCallback } from 'react' -import produce from 'immer' -import type { Var, VarType } from '../../../types' -import Item from './condition-item' -import cn from '@/utils/classnames' -import type { Condition, LogicalOperator } from '@/app/components/workflow/nodes/if-else/types' - -type Props = { - nodeId: string - className?: string - readonly: boolean - list: Condition[] - varTypesList: (VarType | undefined)[] - onChange: (newList: Condition[]) => void - logicalOperator: LogicalOperator - onLogicalOperatorToggle: () => void - filterVar: (varPayload: Var) => boolean -} - -const ConditionList: FC = ({ - className, - readonly, - nodeId, - list, - varTypesList, - onChange, - logicalOperator, - onLogicalOperatorToggle, - filterVar, -}) => { - const handleItemChange = useCallback((index: number) => { - return (newItem: Condition) => { - const newList = produce(list, (draft) => { - draft[index] = newItem - }) - onChange(newList) - } - }, [list, onChange]) - - const handleItemRemove = useCallback((index: number) => { - return () => { - const newList = produce(list, (draft) => { - draft.splice(index, 1) - }) - onChange(newList) - } - }, [list, onChange]) - - const canRemove = list.length > 1 - - if (list.length === 0) - return null - return ( -
- - { - list.length > 1 && ( - list.slice(1).map((item, i) => ( - - ))) - } -
- ) -} -export default React.memo(ConditionList) diff --git a/web/app/components/workflow/nodes/if-else/components/condition-list/condition-input.tsx b/web/app/components/workflow/nodes/if-else/components/condition-list/condition-input.tsx new file mode 100644 index 0000000000..c393aaaa58 --- /dev/null +++ b/web/app/components/workflow/nodes/if-else/components/condition-list/condition-input.tsx @@ -0,0 +1,56 @@ +import { useTranslation } from 'react-i18next' +import { useStore } from '@/app/components/workflow/store' +import PromptEditor from '@/app/components/base/prompt-editor' +import { BlockEnum } from '@/app/components/workflow/types' +import type { + Node, + NodeOutPutVar, +} from '@/app/components/workflow/types' + +type ConditionInputProps = { + disabled?: boolean + value: string + onChange: (value: string) => void + nodesOutputVars: NodeOutPutVar[] + availableNodes: Node[] +} +const ConditionInput = ({ + value, + onChange, + disabled, + nodesOutputVars, + availableNodes, +}: ConditionInputProps) => { + const { t } = useTranslation() + const controlPromptEditorRerenderKey = useStore(s => s.controlPromptEditorRerenderKey) + + return ( + { + acc[node.id] = { + title: node.data.title, + type: node.data.type, + } + if (node.data.type === BlockEnum.Start) { + acc.sys = { + title: t('workflow.blocks.start'), + type: BlockEnum.Start, + } + } + return acc + }, {} as any), + }} + onChange={onChange} + editable={!disabled} + /> + ) +} + +export default ConditionInput diff --git a/web/app/components/workflow/nodes/if-else/components/condition-list/condition-item.tsx b/web/app/components/workflow/nodes/if-else/components/condition-list/condition-item.tsx new file mode 100644 index 0000000000..c6cb580118 --- /dev/null +++ b/web/app/components/workflow/nodes/if-else/components/condition-list/condition-item.tsx @@ -0,0 +1,132 @@ +import { + useCallback, + useState, +} from 'react' +import { RiDeleteBinLine } from '@remixicon/react' +import type { VarType as NumberVarType } from '../../../tool/types' +import type { + ComparisonOperator, + Condition, + HandleRemoveCondition, + HandleUpdateCondition, +} from '../../types' +import { comparisonOperatorNotRequireValue } from '../../utils' +import ConditionNumberInput from '../condition-number-input' +import ConditionOperator from './condition-operator' +import ConditionInput from './condition-input' +import VariableTag from '@/app/components/workflow/nodes/_base/components/variable-tag' +import type { + Node, + NodeOutPutVar, +} from '@/app/components/workflow/types' +import { VarType } from '@/app/components/workflow/types' +import cn from '@/utils/classnames' + +type ConditionItemProps = { + disabled?: boolean + caseId: string + condition: Condition + onRemoveCondition: HandleRemoveCondition + onUpdateCondition: HandleUpdateCondition + nodesOutputVars: NodeOutPutVar[] + availableNodes: Node[] + numberVariables: NodeOutPutVar[] +} +const ConditionItem = ({ + disabled, + caseId, + condition, + onRemoveCondition, + onUpdateCondition, + nodesOutputVars, + availableNodes, + numberVariables, +}: ConditionItemProps) => { + const [isHovered, setIsHovered] = useState(false) + + const handleUpdateConditionOperator = useCallback((value: ComparisonOperator) => { + const newCondition = { + ...condition, + comparison_operator: value, + } + onUpdateCondition(caseId, condition.id, newCondition) + }, [caseId, condition, onUpdateCondition]) + + const handleUpdateConditionValue = useCallback((value: string) => { + const newCondition = { + ...condition, + value, + } + onUpdateCondition(caseId, condition.id, newCondition) + }, [caseId, condition, onUpdateCondition]) + + const handleUpdateConditionNumberVarType = useCallback((numberVarType: NumberVarType) => { + const newCondition = { + ...condition, + numberVarType, + value: '', + } + onUpdateCondition(caseId, condition.id, newCondition) + }, [caseId, condition, onUpdateCondition]) + + return ( +
+
+
+
+ +
+
+ +
+ { + !comparisonOperatorNotRequireValue(condition.comparison_operator) && condition.varType !== VarType.number && ( +
+ +
+ ) + } + { + !comparisonOperatorNotRequireValue(condition.comparison_operator) && condition.varType === VarType.number && ( +
+ +
+ ) + } +
+
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + onClick={() => onRemoveCondition(caseId, condition.id)} + > + +
+
+ ) +} + +export default ConditionItem diff --git a/web/app/components/workflow/nodes/if-else/components/condition-list/condition-operator.tsx b/web/app/components/workflow/nodes/if-else/components/condition-list/condition-operator.tsx new file mode 100644 index 0000000000..3ae1a93b0a --- /dev/null +++ b/web/app/components/workflow/nodes/if-else/components/condition-list/condition-operator.tsx @@ -0,0 +1,91 @@ +import { + useMemo, + useState, +} from 'react' +import { useTranslation } from 'react-i18next' +import { RiArrowDownSLine } from '@remixicon/react' +import { getOperators, isComparisonOperatorNeedTranslate } from '../../utils' +import type { ComparisonOperator } from '../../types' +import Button from '@/app/components/base/button' +import { + PortalToFollowElem, + PortalToFollowElemContent, + PortalToFollowElemTrigger, +} from '@/app/components/base/portal-to-follow-elem' +import type { VarType } from '@/app/components/workflow/types' +import cn from '@/utils/classnames' +const i18nPrefix = 'workflow.nodes.ifElse' + +type ConditionOperatorProps = { + disabled?: boolean + varType: VarType + value?: string + onSelect: (value: ComparisonOperator) => void +} +const ConditionOperator = ({ + disabled, + varType, + value, + onSelect, +}: ConditionOperatorProps) => { + const { t } = useTranslation() + const [open, setOpen] = useState(false) + + const options = useMemo(() => { + return getOperators(varType).map((o) => { + return { + label: isComparisonOperatorNeedTranslate(o) ? t(`${i18nPrefix}.comparisonOperator.${o}`) : o, + value: o, + } + }) + }, [t, varType]) + const selectedOption = options.find(o => o.value === value) + + return ( + + setOpen(v => !v)}> + + + +
+ { + options.map(option => ( +
{ + onSelect(option.value) + setOpen(false) + }} + > + {option.label} +
+ )) + } +
+
+
+ ) +} + +export default ConditionOperator diff --git a/web/app/components/workflow/nodes/if-else/components/condition-list/index.tsx b/web/app/components/workflow/nodes/if-else/components/condition-list/index.tsx new file mode 100644 index 0000000000..b97b0a05ac --- /dev/null +++ b/web/app/components/workflow/nodes/if-else/components/condition-list/index.tsx @@ -0,0 +1,75 @@ +import { RiLoopLeftLine } from '@remixicon/react' +import { LogicalOperator } from '../../types' +import type { + CaseItem, + HandleRemoveCondition, + HandleUpdateCondition, + HandleUpdateConditionLogicalOperator, +} from '../../types' +import ConditionItem from './condition-item' +import type { + Node, + NodeOutPutVar, +} from '@/app/components/workflow/types' + +type ConditionListProps = { + disabled?: boolean + caseItem: CaseItem + onUpdateCondition: HandleUpdateCondition + onUpdateConditionLogicalOperator: HandleUpdateConditionLogicalOperator + onRemoveCondition: HandleRemoveCondition + nodesOutputVars: NodeOutPutVar[] + availableNodes: Node[] + numberVariables: NodeOutPutVar[] +} +const ConditionList = ({ + disabled, + caseItem, + onUpdateCondition, + onUpdateConditionLogicalOperator, + onRemoveCondition, + nodesOutputVars, + availableNodes, + numberVariables, +}: ConditionListProps) => { + const { conditions, logical_operator } = caseItem + + return ( +
+ { + conditions.length > 1 && ( +
+
+
+
{ + onUpdateConditionLogicalOperator(caseItem.case_id, caseItem.logical_operator === LogicalOperator.and ? LogicalOperator.or : LogicalOperator.and) + }} + > + {logical_operator.toUpperCase()} + +
+
+ ) + } + { + caseItem.conditions.map(condition => ( + + )) + } +
+ ) +} + +export default ConditionList diff --git a/web/app/components/workflow/nodes/if-else/components/condition-number-input.tsx b/web/app/components/workflow/nodes/if-else/components/condition-number-input.tsx new file mode 100644 index 0000000000..c8c1616e25 --- /dev/null +++ b/web/app/components/workflow/nodes/if-else/components/condition-number-input.tsx @@ -0,0 +1,153 @@ +import { + memo, + useCallback, + useState, +} from 'react' +import { useTranslation } from 'react-i18next' +import { RiArrowDownSLine } from '@remixicon/react' +import { capitalize } from 'lodash-es' +import { VarType as NumberVarType } from '../../tool/types' +import VariableTag from '../../_base/components/variable-tag' +import { + PortalToFollowElem, + PortalToFollowElemContent, + PortalToFollowElemTrigger, +} from '@/app/components/base/portal-to-follow-elem' +import Button from '@/app/components/base/button' +import cn from '@/utils/classnames' +import VarReferenceVars from '@/app/components/workflow/nodes/_base/components/variable/var-reference-vars' +import type { + NodeOutPutVar, + ValueSelector, +} from '@/app/components/workflow/types' +import { VarType } from '@/app/components/workflow/types' +import { variableTransformer } from '@/app/components/workflow/utils' +import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development' + +const options = [ + NumberVarType.variable, + NumberVarType.constant, +] + +type ConditionNumberInputProps = { + numberVarType?: NumberVarType + onNumberVarTypeChange: (v: NumberVarType) => void + value: string + onValueChange: (v: string) => void + variables: NodeOutPutVar[] +} +const ConditionNumberInput = ({ + numberVarType = NumberVarType.constant, + onNumberVarTypeChange, + value, + onValueChange, + variables, +}: ConditionNumberInputProps) => { + const { t } = useTranslation() + const [numberVarTypeVisible, setNumberVarTypeVisible] = useState(false) + const [variableSelectorVisible, setVariableSelectorVisible] = useState(false) + + const handleSelectVariable = useCallback((valueSelector: ValueSelector) => { + onValueChange(variableTransformer(valueSelector) as string) + setVariableSelectorVisible(false) + }, [onValueChange]) + + return ( +
+ + setNumberVarTypeVisible(v => !v)}> + + + +
+ { + options.map(option => ( +
{ + onNumberVarTypeChange(option) + setNumberVarTypeVisible(false) + }} + > + {capitalize(option)} +
+ )) + } +
+
+
+
+
+ { + numberVarType === NumberVarType.variable && ( + + setVariableSelectorVisible(v => !v)}> + { + value && ( + + ) + } + { + !value && ( +
+ + {t('workflow.nodes.ifElse.selectVariable')} +
+ ) + } +
+ +
+ +
+
+
+ ) + } + { + numberVarType === NumberVarType.constant && ( + onValueChange(e.target.value)} + placeholder={t('workflow.nodes.ifElse.enterValue') || ''} + /> + ) + } +
+
+ ) +} + +export default memo(ConditionNumberInput) diff --git a/web/app/components/workflow/nodes/if-else/components/condition-value.tsx b/web/app/components/workflow/nodes/if-else/components/condition-value.tsx new file mode 100644 index 0000000000..904ecc8e81 --- /dev/null +++ b/web/app/components/workflow/nodes/if-else/components/condition-value.tsx @@ -0,0 +1,70 @@ +import { + memo, + useMemo, +} from 'react' +import { useTranslation } from 'react-i18next' +import type { ComparisonOperator } from '../types' +import { + comparisonOperatorNotRequireValue, + isComparisonOperatorNeedTranslate, +} from '../utils' +import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development' +import cn from '@/utils/classnames' +import { isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils' + +type ConditionValueProps = { + variableSelector: string[] + operator: ComparisonOperator + value: string +} +const ConditionValue = ({ + variableSelector, + operator, + value, +}: ConditionValueProps) => { + const { t } = useTranslation() + const variableName = isSystemVar(variableSelector) ? variableSelector.slice(0).join('.') : variableSelector.slice(1).join('.') + const operatorName = isComparisonOperatorNeedTranslate(operator) ? t(`workflow.nodes.ifElse.comparisonOperator.${operator}`) : operator + const notHasValue = comparisonOperatorNotRequireValue(operator) + + const formatValue = useMemo(() => { + if (notHasValue) + return '' + + return value.replace(/{{#([^#]*)#}}/g, (a, b) => { + const arr = b.split('.') + if (isSystemVar(arr)) + return `{{${b}}}` + + return `{{${arr.slice(1).join('.')}}}` + }) + }, [notHasValue, value]) + + return ( +
+ +
+ {variableName} +
+
+ {operatorName} +
+ { + !notHasValue && ( +
{formatValue}
+ ) + } +
+ ) +} + +export default memo(ConditionValue) diff --git a/web/app/components/workflow/nodes/if-else/default.ts b/web/app/components/workflow/nodes/if-else/default.ts index befd2de2e1..af65c7b46c 100644 --- a/web/app/components/workflow/nodes/if-else/default.ts +++ b/web/app/components/workflow/nodes/if-else/default.ts @@ -9,15 +9,20 @@ const nodeDefault: NodeDefault = { _targetBranches: [ { id: 'true', - name: 'IS TRUE', + name: 'IF', }, { id: 'false', - name: 'IS FALSE', + name: 'ELSE', + }, + ], + cases: [ + { + case_id: 'true', + logical_operator: LogicalOperator.and, + conditions: [], }, ], - logical_operator: LogicalOperator.and, - conditions: [], }, getAvailablePrevNodes(isChatMode: boolean) { const nodes = isChatMode @@ -31,17 +36,22 @@ const nodeDefault: NodeDefault = { }, checkValid(payload: IfElseNodeType, t: any) { let errorMessages = '' - const { conditions } = payload - if (!conditions || conditions.length === 0) + const { cases } = payload + if (!cases || cases.length === 0) errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: 'IF' }) - conditions.forEach((condition) => { - if (!errorMessages && (!condition.variable_selector || condition.variable_selector.length === 0)) - errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t(`${i18nPrefix}.fields.variable`) }) - if (!errorMessages && !condition.comparison_operator) - errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t('workflow.nodes.ifElse.operator') }) - if (!errorMessages && !isEmptyRelatedOperator(condition.comparison_operator!) && !condition.value) - errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t(`${i18nPrefix}.fields.variableValue`) }) + cases.forEach((caseItem, index) => { + if (!caseItem.conditions.length) + errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: index === 0 ? 'IF' : 'ELIF' }) + + caseItem.conditions.forEach((condition) => { + if (!errorMessages && (!condition.variable_selector || condition.variable_selector.length === 0)) + errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t(`${i18nPrefix}.fields.variable`) }) + if (!errorMessages && !condition.comparison_operator) + errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t('workflow.nodes.ifElse.operator') }) + if (!errorMessages && !isEmptyRelatedOperator(condition.comparison_operator!) && !condition.value) + errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: t(`${i18nPrefix}.fields.variableValue`) }) + }) }) return { isValid: !errorMessages, diff --git a/web/app/components/workflow/nodes/if-else/node.tsx b/web/app/components/workflow/nodes/if-else/node.tsx index bb062d991e..67ce6529a6 100644 --- a/web/app/components/workflow/nodes/if-else/node.tsx +++ b/web/app/components/workflow/nodes/if-else/node.tsx @@ -3,51 +3,62 @@ import React from 'react' import { useTranslation } from 'react-i18next' import type { NodeProps } from 'reactflow' import { NodeSourceHandle } from '../_base/components/node-handle' -import { isComparisonOperatorNeedTranslate, isEmptyRelatedOperator } from './utils' +import { isEmptyRelatedOperator } from './utils' import type { IfElseNodeType } from './types' -import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development' +import ConditionValue from './components/condition-value' const i18nPrefix = 'workflow.nodes.ifElse' const IfElseNode: FC> = (props) => { const { data } = props const { t } = useTranslation() - const { conditions, logical_operator } = data + const { cases } = data + const casesLength = cases.length return (
-
-
IF
- -
-
- {conditions.map((condition, i) => ( -
- {(condition.variable_selector?.length > 0 && condition.comparison_operator && (isEmptyRelatedOperator(condition.comparison_operator!) ? true : !!condition.value)) - ? ( -
- - {condition.variable_selector.slice(-1)[0]} - {isComparisonOperatorNeedTranslate(condition.comparison_operator) ? t(`${i18nPrefix}.comparisonOperator.${condition.comparison_operator}`) : condition.comparison_operator} - {!isEmptyRelatedOperator(condition.comparison_operator!) && {condition.value}} + { + cases.map((caseItem, index) => ( +
+
+
+
+ {casesLength > 1 && `CASE ${index + 1}`}
- ) - : ( -
- {t(`${i18nPrefix}.conditionNotSetup`)} +
{index === 0 ? 'IF' : 'ELIF'}
+
+ +
+
+ {caseItem.conditions.map((condition, i) => ( +
+ {(condition.variable_selector?.length > 0 && condition.comparison_operator && (isEmptyRelatedOperator(condition.comparison_operator!) ? true : !!condition.value)) + ? ( + + ) + : ( +
+ {t(`${i18nPrefix}.conditionNotSetup`)} +
+ )} + {i !== caseItem.conditions.length - 1 && ( +
{t(`${i18nPrefix}.${caseItem.logical_operator}`)}
+ )}
- )} - {i !== conditions.length - 1 && ( -
{t(`${i18nPrefix}.${logical_operator}`)}
- )} + ))} +
- ))} -
+ )) + }
-
ELSE
+
ELSE
> = ({ @@ -15,52 +26,130 @@ const Panel: FC> = ({ data, }) => { const { t } = useTranslation() - + const getAvailableVars = useGetAvailableVars() const { readOnly, inputs, - handleConditionsChange, - handleAddCondition, - handleLogicalOperatorToggle, - varTypesList, filterVar, + filterNumberVar, + handleAddCase, + handleRemoveCase, + handleSortCase, + handleAddCondition, + handleUpdateCondition, + handleRemoveCondition, + handleUpdateConditionLogicalOperator, + nodesOutputVars, + availableNodes, } = useConfig(id, data) + const [willDeleteCaseId, setWillDeleteCaseId] = useState('') + const cases = inputs.cases || [] + const casesLength = cases.length + return ( -
-
- + ({ ...caseItem, id: caseItem.case_id }))} + setList={handleSortCase} + handle='.handle' + ghostClass='bg-components-panel-bg' + animation={150} + > + { + cases.map((item, index) => ( +
+
+ 1 && 'group-hover:block', + )} /> +
+ { + index === 0 ? 'IF' : 'ELIF' + } + { + casesLength > 1 && ( +
CASE {index + 1}
+ ) + } +
+ { + !!item.conditions.length && ( +
+ +
+ ) + } +
+ + { + ((index === 0 && casesLength > 1) || (index > 0)) && ( + + ) + } +
+
+
+
+ )) + } +
+
+
+
+ +
{t(`${i18nPrefix}.elseDescription`)}
+
) } -export default React.memo(Panel) +export default memo(Panel) diff --git a/web/app/components/workflow/nodes/if-else/types.ts b/web/app/components/workflow/nodes/if-else/types.ts index 45adf375e5..693dce1784 100644 --- a/web/app/components/workflow/nodes/if-else/types.ts +++ b/web/app/components/workflow/nodes/if-else/types.ts @@ -1,4 +1,10 @@ -import type { CommonNodeType, ValueSelector } from '@/app/components/workflow/types' +import type { VarType as NumberVarType } from '../tool/types' +import type { + CommonNodeType, + ValueSelector, + Var, + VarType, +} from '@/app/components/workflow/types' export enum LogicalOperator { and = 'and', @@ -26,12 +32,26 @@ export enum ComparisonOperator { export type Condition = { id: string + varType: VarType variable_selector: ValueSelector comparison_operator?: ComparisonOperator value: string + numberVarType?: NumberVarType } -export type IfElseNodeType = CommonNodeType & { +export type CaseItem = { + case_id: string logical_operator: LogicalOperator conditions: Condition[] } + +export type IfElseNodeType = CommonNodeType & { + logical_operator?: LogicalOperator + conditions?: Condition[] + cases: CaseItem[] +} + +export type HandleAddCondition = (caseId: string, valueSelector: ValueSelector, varItem: Var) => void +export type HandleRemoveCondition = (caseId: string, conditionId: string) => void +export type HandleUpdateCondition = (caseId: string, conditionId: string, newCondition: Condition) => void +export type HandleUpdateConditionLogicalOperator = (caseId: string, value: LogicalOperator) => void diff --git a/web/app/components/workflow/nodes/if-else/use-config.ts b/web/app/components/workflow/nodes/if-else/use-config.ts index b20a5b56ea..d3e2785986 100644 --- a/web/app/components/workflow/nodes/if-else/use-config.ts +++ b/web/app/components/workflow/nodes/if-else/use-config.ts @@ -1,76 +1,177 @@ import { useCallback } from 'react' import produce from 'immer' -import type { Var } from '../../types' +import { v4 as uuid4 } from 'uuid' +import type { + Var, +} from '../../types' import { VarType } from '../../types' -import { getVarType } from '../_base/components/variable/utils' -import useNodeInfo from '../_base/hooks/use-node-info' import { LogicalOperator } from './types' -import type { Condition, IfElseNodeType } from './types' +import type { + CaseItem, + HandleAddCondition, + HandleRemoveCondition, + HandleUpdateCondition, + HandleUpdateConditionLogicalOperator, + IfElseNodeType, +} from './types' +import { + branchNameCorrect, + getOperators, +} from './utils' import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud' import { - useIsChatMode, + useEdgesInteractions, useNodesReadOnly, - useWorkflow, } from '@/app/components/workflow/hooks' +import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list' const useConfig = (id: string, payload: IfElseNodeType) => { const { nodesReadOnly: readOnly } = useNodesReadOnly() - const { getBeforeNodesInSameBranch } = useWorkflow() - const { - parentNode, - } = useNodeInfo(id) - const isChatMode = useIsChatMode() - const beforeNodes = getBeforeNodesInSameBranch(id) - + const { handleEdgeDeleteByDeleteBranch } = useEdgesInteractions() const { inputs, setInputs } = useNodeCrud(id, payload) - const handleConditionsChange = useCallback((newConditions: Condition[]) => { - const newInputs = produce(inputs, (draft) => { - draft.conditions = newConditions - }) - setInputs(newInputs) - }, [inputs, setInputs]) - - const handleAddCondition = useCallback(() => { - const newInputs = produce(inputs, (draft) => { - draft.conditions.push({ - id: `${Date.now()}`, - variable_selector: [], - comparison_operator: undefined, - value: '', - }) - }) - setInputs(newInputs) - }, [inputs, setInputs]) - - const handleLogicalOperatorToggle = useCallback(() => { - const newInputs = produce(inputs, (draft) => { - draft.logical_operator = draft.logical_operator === LogicalOperator.and ? LogicalOperator.or : LogicalOperator.and - }) - setInputs(newInputs) - }, [inputs, setInputs]) - const filterVar = useCallback((varPayload: Var) => { return varPayload.type !== VarType.arrayFile }, []) - const varTypesList = (inputs.conditions || []).map((condition) => { - return getVarType({ - parentNode, - valueSelector: condition.variable_selector, - availableNodes: beforeNodes, - isChatMode, - }) + const { + availableVars, + availableNodesWithParent, + } = useAvailableVarList(id, { + onlyLeafNodeVar: false, + filterVar, }) + const filterNumberVar = useCallback((varPayload: Var) => { + return varPayload.type === VarType.number + }, []) + + const { + availableVars: availableNumberVars, + availableNodesWithParent: availableNumberNodesWithParent, + } = useAvailableVarList(id, { + onlyLeafNodeVar: false, + filterVar: filterNumberVar, + }) + + const handleAddCase = useCallback(() => { + const newInputs = produce(inputs, () => { + if (inputs.cases) { + const case_id = uuid4() + inputs.cases.push({ + case_id, + logical_operator: LogicalOperator.and, + conditions: [], + }) + if (inputs._targetBranches) { + const elseCaseIndex = inputs._targetBranches.findIndex(branch => branch.id === 'false') + if (elseCaseIndex > -1) { + inputs._targetBranches = branchNameCorrect([ + ...inputs._targetBranches.slice(0, elseCaseIndex), + { + id: case_id, + name: '', + }, + ...inputs._targetBranches.slice(elseCaseIndex), + ]) + } + } + } + }) + setInputs(newInputs) + }, [inputs, setInputs]) + + const handleRemoveCase = useCallback((caseId: string) => { + const newInputs = produce(inputs, (draft) => { + draft.cases = draft.cases?.filter(item => item.case_id !== caseId) + + if (draft._targetBranches) + draft._targetBranches = branchNameCorrect(draft._targetBranches.filter(branch => branch.id !== caseId)) + + handleEdgeDeleteByDeleteBranch(id, caseId) + }) + setInputs(newInputs) + }, [inputs, setInputs, id, handleEdgeDeleteByDeleteBranch]) + + const handleSortCase = useCallback((newCases: (CaseItem & { id: string })[]) => { + const newInputs = produce(inputs, (draft) => { + draft.cases = newCases.filter(Boolean).map(item => ({ + id: item.id, + case_id: item.case_id, + logical_operator: item.logical_operator, + conditions: item.conditions, + })) + + draft._targetBranches = branchNameCorrect([ + ...newCases.filter(Boolean).map(item => ({ id: item.case_id, name: '' })), + { id: 'false', name: '' }, + ]) + }) + setInputs(newInputs) + }, [inputs, setInputs]) + + const handleAddCondition = useCallback((caseId, valueSelector, varItem) => { + const newInputs = produce(inputs, (draft) => { + const targetCase = draft.cases?.find(item => item.case_id === caseId) + if (targetCase) { + targetCase.conditions.push({ + id: uuid4(), + varType: varItem.type, + variable_selector: valueSelector, + comparison_operator: getOperators(varItem.type)[0], + value: '', + }) + } + }) + setInputs(newInputs) + }, [inputs, setInputs]) + + const handleRemoveCondition = useCallback((caseId, conditionId) => { + const newInputs = produce(inputs, (draft) => { + const targetCase = draft.cases?.find(item => item.case_id === caseId) + if (targetCase) + targetCase.conditions = targetCase.conditions.filter(item => item.id !== conditionId) + }) + setInputs(newInputs) + }, [inputs, setInputs]) + + const handleUpdateCondition = useCallback((caseId, conditionId, newCondition) => { + const newInputs = produce(inputs, (draft) => { + const targetCase = draft.cases?.find(item => item.case_id === caseId) + if (targetCase) { + const targetCondition = targetCase.conditions.find(item => item.id === conditionId) + if (targetCondition) + Object.assign(targetCondition, newCondition) + } + }) + setInputs(newInputs) + }, [inputs, setInputs]) + + const handleUpdateConditionLogicalOperator = useCallback((caseId, value) => { + const newInputs = produce(inputs, (draft) => { + const targetCase = draft.cases?.find(item => item.case_id === caseId) + if (targetCase) + targetCase.logical_operator = value + }) + setInputs(newInputs) + }, [inputs, setInputs]) + return { readOnly, inputs, - handleConditionsChange, - handleAddCondition, - handleLogicalOperatorToggle, - varTypesList, filterVar, + filterNumberVar, + handleAddCase, + handleRemoveCase, + handleSortCase, + handleAddCondition, + handleRemoveCondition, + handleUpdateCondition, + handleUpdateConditionLogicalOperator, + nodesOutputVars: availableVars, + availableNodes: availableNodesWithParent, + nodesOutputNumberVars: availableNumberVars, + availableNumberNodes: availableNumberNodesWithParent, } } diff --git a/web/app/components/workflow/nodes/if-else/utils.ts b/web/app/components/workflow/nodes/if-else/utils.ts index 51858c64aa..ffb6758bba 100644 --- a/web/app/components/workflow/nodes/if-else/utils.ts +++ b/web/app/components/workflow/nodes/if-else/utils.ts @@ -1,4 +1,6 @@ import { ComparisonOperator } from './types' +import { VarType } from '@/app/components/workflow/types' +import type { Branch } from '@/app/components/workflow/types' export const isEmptyRelatedOperator = (operator: ComparisonOperator) => { return [ComparisonOperator.empty, ComparisonOperator.notEmpty, ComparisonOperator.isNull, ComparisonOperator.isNotNull].includes(operator) @@ -15,3 +17,80 @@ export const isComparisonOperatorNeedTranslate = (operator?: ComparisonOperator) return false return !notTranslateKey.includes(operator) } + +export const getOperators = (type?: VarType) => { + switch (type) { + case VarType.string: + return [ + ComparisonOperator.contains, + ComparisonOperator.notContains, + ComparisonOperator.startWith, + ComparisonOperator.endWith, + ComparisonOperator.is, + ComparisonOperator.isNot, + ComparisonOperator.empty, + ComparisonOperator.notEmpty, + ] + case VarType.number: + return [ + ComparisonOperator.equal, + ComparisonOperator.notEqual, + ComparisonOperator.largerThan, + ComparisonOperator.lessThan, + ComparisonOperator.largerThanOrEqual, + ComparisonOperator.lessThanOrEqual, + ComparisonOperator.empty, + ComparisonOperator.notEmpty, + ] + case VarType.arrayString: + case VarType.arrayNumber: + return [ + ComparisonOperator.contains, + ComparisonOperator.notContains, + ComparisonOperator.empty, + ComparisonOperator.notEmpty, + ] + case VarType.array: + case VarType.arrayObject: + return [ + ComparisonOperator.empty, + ComparisonOperator.notEmpty, + ] + default: + return [ + ComparisonOperator.is, + ComparisonOperator.isNot, + ComparisonOperator.empty, + ComparisonOperator.notEmpty, + ] + } +} + +export const comparisonOperatorNotRequireValue = (operator?: ComparisonOperator) => { + if (!operator) + return false + + return [ComparisonOperator.empty, ComparisonOperator.notEmpty, ComparisonOperator.isNull, ComparisonOperator.isNotNull].includes(operator) +} + +export const branchNameCorrect = (branches: Branch[]) => { + const branchLength = branches.length + if (branchLength < 2) + throw new Error('if-else node branch number must than 2') + + if (branchLength === 2) { + return branches.map((branch) => { + return { + ...branch, + name: branch.id === 'false' ? 'ELSE' : 'IF', + } + }) + } + + return branches.map((branch, index) => { + return { + ...branch, + name: branch.id === 'false' ? 'ELSE' : `CASE ${index + 1}`, + } + }) +} diff --git a/web/app/components/workflow/utils.ts b/web/app/components/workflow/utils.ts index 4ad9c6591c..0d07b2e568 100644 --- a/web/app/components/workflow/utils.ts +++ b/web/app/components/workflow/utils.ts @@ -14,6 +14,7 @@ import type { InputVar, Node, ToolWithProvider, + ValueSelector, } from './types' import { BlockEnum } from './types' import { @@ -23,6 +24,8 @@ import { START_INITIAL_POSITION, } from './constants' import type { QuestionClassifierNodeType } from './nodes/question-classifier/types' +import type { IfElseNodeType } from './nodes/if-else/types' +import { branchNameCorrect } from './nodes/if-else/utils' import type { ToolNodeType } from './nodes/tool/types' import { CollectionType } from '@/app/components/tools/types' import { toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema' @@ -114,16 +117,21 @@ export const initialNodes = (originNodes: Node[], originEdges: Edge[]) => { node.data._connectedTargetHandleIds = connectedEdges.filter(edge => edge.target === node.id).map(edge => edge.targetHandle || 'target') if (node.data.type === BlockEnum.IfElse) { - node.data._targetBranches = [ - { - id: 'true', - name: 'IS TRUE', - }, - { - id: 'false', - name: 'IS FALSE', - }, - ] + const nodeData = node.data as IfElseNodeType + + if (!nodeData.cases && nodeData.logical_operator && nodeData.conditions) { + (node.data as IfElseNodeType).cases = [ + { + case_id: 'true', + logical_operator: nodeData.logical_operator, + conditions: nodeData.conditions, + }, + ] + } + node.data._targetBranches = branchNameCorrect([ + ...(node.data as IfElseNodeType).cases.map(item => ({ id: item.case_id, name: '' })), + { id: 'false', name: '' }, + ]) } if (node.data.type === BlockEnum.QuestionClassifier) { @@ -184,6 +192,7 @@ export const initialEdges = (originEdges: Edge[], originNodes: Node[]) => { _connectedNodeIsSelected: edge.source === selectedNode.id || edge.target === selectedNode.id, } as any } + return edge }) } @@ -463,3 +472,10 @@ export const isEventTargetInputArea = (target: HTMLElement) => { if (target.contentEditable === 'true') return true } + +export const variableTransformer = (v: ValueSelector | string) => { + if (typeof v === 'string') + return v.replace(/^{{#|#}}$/g, '').split('.') + + return `{{#${v.join('.')}#}}` +} diff --git a/web/i18n/en-US/workflow.ts b/web/i18n/en-US/workflow.ts index 4ac3e82a95..568823bb3a 100644 --- a/web/i18n/en-US/workflow.ts +++ b/web/i18n/en-US/workflow.ts @@ -364,6 +364,7 @@ const translation = { enterValue: 'Enter value', addCondition: 'Add Condition', conditionNotSetup: 'Condition NOT setup', + selectVariable: 'Select variable...', }, variableAssigner: { title: 'Assign variables', diff --git a/web/i18n/zh-Hans/workflow.ts b/web/i18n/zh-Hans/workflow.ts index a71b22c8e0..2b9af83f6c 100644 --- a/web/i18n/zh-Hans/workflow.ts +++ b/web/i18n/zh-Hans/workflow.ts @@ -364,6 +364,7 @@ const translation = { enterValue: '输入值', addCondition: '添加条件', conditionNotSetup: '条件未设置', + selectVariable: '选择变量', }, variableAssigner: { title: '变量赋值',