diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index 2e4ba2b4..dfc42973 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -2389,6 +2389,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re else_desc: 'Used to define the logic that should be executed when the if condition is not met.', unset: 'Condition Not Set', set: 'Set', + addSubVariable: 'Add Sub Variable', }, 'http-request': { auth: 'Authentication', diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index 6efcf72d..ae0181c9 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -2350,6 +2350,7 @@ export const zh = { else_desc: '用于定义当 if 条件不满足时应执行的逻辑。', unset: '条件未设置', set: '已设置', + addSubVariable: '添加子变量', }, 'http-request': { auth: '鉴权', diff --git a/web/src/views/UserMemoryDetail/components/EndUserProfile.tsx b/web/src/views/UserMemoryDetail/components/EndUserProfile.tsx index 1e8ef366..702d152e 100644 --- a/web/src/views/UserMemoryDetail/components/EndUserProfile.tsx +++ b/web/src/views/UserMemoryDetail/components/EndUserProfile.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-03 18:33:30 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-04-14 16:03:41 + * @Last Modified time: 2026-04-17 17:57:15 */ /** * End User Profile Component @@ -89,11 +89,11 @@ const EndUserProfile = forwardRef(({ cla
{t('userMemory.role')}
-
{data?.profile?.role || '-'}
+
{data?.profile?.role?.join(' | ') || '-'}
{t('userMemory.domain')}
-
{data?.profile?.domain || '-'}
+
{data?.profile?.domain?.join(' | ') || '-'}
{t('userMemory.expertise')}
diff --git a/web/src/views/UserMemoryDetail/types.ts b/web/src/views/UserMemoryDetail/types.ts index 4eb4fe5b..667d8272 100644 --- a/web/src/views/UserMemoryDetail/types.ts +++ b/web/src/views/UserMemoryDetail/types.ts @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-03 17:57:15 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-04-14 16:03:16 + * @Last Modified time: 2026-04-17 17:57:00 */ /** * User Memory Detail Types @@ -178,8 +178,8 @@ export interface EndUser { created_at: string; updated_at: string; profile: { - role: string; - domain: string; + role: string[]; + domain: string[]; expertise: string[]; interests: string[]; }; diff --git a/web/src/views/Workflow/components/CheckList/index.tsx b/web/src/views/Workflow/components/CheckList/index.tsx index 62132a71..4afdb863 100644 --- a/web/src/views/Workflow/components/CheckList/index.tsx +++ b/web/src/views/Workflow/components/CheckList/index.tsx @@ -6,6 +6,7 @@ import { Node } from '@antv/x6'; import type { WorkflowRef } from '@/views/ApplicationConfig/types' import { nodeLibrary } from '../../constant' +import { isSubExprSet } from '../../utils' import { getToolMethods } from '@/api/tools' import RbDrawer from '@/components/RbDrawer' import { useWorkflowStore } from '@/store/workflow' @@ -44,14 +45,13 @@ const specialValidators: Record boolean> = { // if-else.cases: every case must have at least one expression, and every expression must be fully set 'if-else.cases': (val: any[]) => { if (!Array.isArray(val) || !val.length) return true - return val.some(c => { - if (!c?.expressions?.length) return true - return c.expressions.some((expr: any) => { - if (!expr?.left) return true - if (['not_empty', 'empty'].includes(expr.operator)) return false - return !(!!expr.left && (!!expr.right || typeof expr.right === 'boolean' || typeof expr.right === 'number')) - }) - }) + const isExprSet = (expr: any) => { + if (expr?.sub_variable_condition?.conditions?.length > 0) return expr.sub_variable_condition?.conditions.every(isSubExprSet) + if (!expr.left) return false + if (['not_empty', 'empty'].includes(expr.operator)) return true + return !!expr.left && (!!expr.right || typeof expr.right === 'boolean' || typeof expr.right === 'number') + } + return val.some(c => !c?.expressions?.length || c.expressions.some((expr: any) => !isExprSet(expr))) }, // question-classifier.categories: every category must have a value 'question-classifier.categories': (val: any[]) => !Array.isArray(val) || !val.some(c => c?.class_name && String(c.class_name).trim()), diff --git a/web/src/views/Workflow/components/Nodes/ConditionNode.tsx b/web/src/views/Workflow/components/Nodes/ConditionNode.tsx index 625a1b4d..5e62775d 100644 --- a/web/src/views/Workflow/components/Nodes/ConditionNode.tsx +++ b/web/src/views/Workflow/components/Nodes/ConditionNode.tsx @@ -6,12 +6,17 @@ import { Flex } from 'antd'; import NodeTools from './NodeTools' import { useVariableList } from '../Properties/hooks/useVariableList' +import { isSubExprSet } from '../../utils' +import { fileSubFieldOperators } from '../Properties/CaseList' const caculateIsSet = (item: any, type: string) => { switch (type) { case 'categories': return typeof item?.class_name === 'string' && item?.class_name !== '' case 'cases': { + if (item?.sub_variable_condition !== undefined) { + return !!item.left && !!item.operator + } if (!item.left) return false if (['not_empty', 'empty'].includes(item.operator)) return true return !!item.left && (!!item.right || typeof item.right === 'boolean' || typeof item.right === 'number') @@ -25,10 +30,20 @@ const ConditionNode: ReactShapeConfig['component'] = ({ node }) => { const variableList = useVariableList(node ?? null, graphRef, data.chatVariables ?? []) const getLocaleField = (field: string, filedType: string) => { - const key = filedType === 'boolean' ? `workflow.config.if-else..boolean.${field}` : filedType === 'number' ? `workflow.config.if-else.num.${field}` : `workflow.config.if-else.${field}` + const key = filedType === 'boolean' + ? `workflow.config.if-else..boolean.${field}` + : filedType === 'number' + ? `workflow.config.if-else.num.${field}` + : `workflow.config.if-else.${field}` const value = t(key) return value !== key ? value : t(`workflow.config.if-else.num.${field}`) }; + const getSubLocaleField = (field: string, fieldKey: string) => { + const operators = fileSubFieldOperators[fieldKey] ?? fileSubFieldOperators.default + const match = operators?.find(op => op.value === field) + return match?.label ? t(match.label as string) : field + } + const labelRender = (value: string) => { const filterOption = variableList.find(vo => `{{${vo.value}}}` === value) ?? variableList.flatMap(vo => vo.children ?? []).find(child => `{{${child.value}}}` === value) @@ -82,7 +97,8 @@ const ConditionNode: ReactShapeConfig['component'] = ({ node }) => { {item.expressions.map((expression: any, eIndex: number) => (
{item.expressions.length > 1 && eIndex > 0 &&
{item.logical_operator?.toLocaleUpperCase()}
} - + + {caculateIsSet(expression, 'cases') ? <> {labelRender(expression.left)} @@ -91,6 +107,33 @@ const ConditionNode: ReactShapeConfig['component'] = ({ node }) => { : t(`workflow.config.${data.type}.unset`) } + + {expression.sub_variable_condition?.conditions?.length > 0 && expression.sub_variable_condition?.conditions.every(isSubExprSet) + ?
+ {expression.sub_variable_condition?.conditions.map((sub: any, sIndex: number) => ( +
+ {expression.sub_variable_condition?.conditions.length > 1 && sIndex > 0 &&
{expression.sub_variable_condition?.logical_operator?.toLocaleUpperCase()}
} + + {sub.key} + {getSubLocaleField(sub.operator, sub.key)} + + {sub.key === 'type' + ? t(`application.${sub.value}`) + :!['not_empty', 'empty'].includes(sub.operator) + ? {typeof sub.value === 'boolean' ? String(sub.value).charAt(0).toUpperCase() + String(sub.value).slice(1) : sub.value} + : null + } + + +
+ ))} +
+ : expression.sub_variable_condition?.conditions?.length > 0 + ? + {t(`workflow.config.${data.type}.unset`)} + + : null + }
))} diff --git a/web/src/views/Workflow/components/Properties/CaseList/index.tsx b/web/src/views/Workflow/components/Properties/CaseList/index.tsx index 02241fed..6eaa08e1 100644 --- a/web/src/views/Workflow/components/Properties/CaseList/index.tsx +++ b/web/src/views/Workflow/components/Properties/CaseList/index.tsx @@ -2,9 +2,9 @@ * @Author: ZhaoYing * @Date: 2026-02-09 18:24:53 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-04-16 12:06:16 + * @Last Modified time: 2026-04-17 19:58:13 */ -import { useMemo, type FC } from 'react' +import { useEffect, useMemo, type FC } from 'react' import clsx from 'clsx' import { useTranslation } from 'react-i18next'; import { Form, Button, Select, Space, Divider, InputNumber, type SelectProps, Flex, Row, Col } from 'antd' @@ -15,11 +15,37 @@ import Editor from '../../Editor' import { edgeAttrs, nodeWidth } from '../../../constant' import RbButton from '@/components/RbButton'; import RadioGroupBtn from '../RadioGroupBtn' -import { calcConditionNodeTotalHeight, getConditionNodeCasePortY } from '../../../utils'; +import { calcConditionNodeTotalHeight, getConditionNodeCasePortY, isSubExprSet } from '../../../utils'; +import { typeOptions } from '../ListOperator/FilterConditions' + +interface SubCondition { + key: string; + operator: string; + value: string | number; + var_type: string; +} + +interface SubVariableCondition { + logical_operator: 'and' | 'or'; + conditions: SubCondition[]; +} + +interface Expression { + left: string; + operator: string; + right?: string | number; + input_type?: string; + sub_variable_condition?: SubVariableCondition; +} + +interface CaseItem { + logical_operator: 'and' | 'or'; + expressions: Expression[]; +} interface CaseListProps { - value?: Array<{ logical_operator: 'and' | 'or'; expressions: { left: string; operator: string; right: string; input_type?: string; }[] }>; - onChange?: (value: Array<{ logical_operator: 'and' | 'or'; expressions: { left: string; operator: string; right: string; }[] }>) => void; + value?: CaseItem[]; + onChange?: (value: CaseItem[]) => void; options: Suggestion[]; name: string; selectedNode?: any; @@ -60,13 +86,12 @@ const operatorsObj: { [key: string]: SelectProps['options'] } = { { value: 'empty', label: 'workflow.config.if-else.file.empty' }, { value: 'not_empty', label: 'workflow.config.if-else.file.not_empty' }, ], - // TODO:包含、不包含、全都是 'array[file]': [ + { value: 'contains', label: 'workflow.config.if-else.contains' }, + { value: 'not_contains', label: 'workflow.config.if-else.not_contains' }, + { value: 'eq', label: 'workflow.config.if-else.file.eq' }, { value: 'empty', label: 'workflow.config.if-else.empty' }, { value: 'not_empty', label: 'workflow.config.if-else.not_empty' }, - // { value: 'eq', label: 'workflow.config.if-else.eq' }, - // { value: 'contains', label: 'workflow.config.if-else.contains' }, - // { value: 'not_contains', label: 'workflow.config.if-else.not_contains' }, ], 'array': [ { value: 'contains', label: 'workflow.config.if-else.contains' }, @@ -80,6 +105,218 @@ const operatorsObj: { [key: string]: SelectProps['options'] } = { ] } +const fileSubFieldOptions = [ + { value: 'type', label: 'type' }, + { value: 'size', label: 'size' }, + { value: 'name', label: 'name' }, + { value: 'url', label: 'url' }, + { value: 'extension', label: 'extension' }, + { value: 'mime_type', label: 'mime_type' }, +] + +export const fileSubFieldOperators: { [key: string]: SelectProps['options'] } = { + type: [ + { value: 'eq', label: 'workflow.config.list-operator.type.eq' }, + { value: 'ne', label: 'workflow.config.list-operator.type.ne' }, + ], + size: [ + { value: 'lt', label: 'workflow.config.if-else.num.lt' }, + { value: 'le', label: 'workflow.config.if-else.num.le' }, + { value: 'gt', label: 'workflow.config.if-else.num.gt' }, + { value: 'ge', label: 'workflow.config.if-else.num.ge' }, + ], + default: [ + { value: 'contains', label: 'workflow.config.if-else.contains' }, + { value: 'not_contains', label: 'workflow.config.if-else.not_contains' }, + { value: 'startwith', label: 'workflow.config.if-else.startwith' }, + { value: 'endwith', label: 'workflow.config.if-else.endwith' }, + { value: 'eq', label: 'workflow.config.if-else.eq' }, + { value: 'ne', label: 'workflow.config.if-else.ne' }, + { value: 'empty', label: 'workflow.config.if-else.empty' }, + { value: 'not_empty', label: 'workflow.config.if-else.not_empty' }, + ], +} + +interface ArrayFileSubConditionsProps { + conditionFieldName: number; + caseIndex: number; + conditionIndex: number; + name: string; + filterNumberOptions: Suggestion[]; + options: Suggestion[]; + updateNodeLayout: (cases: any[]) => void; + updateNodePorts: (caseCount: number, removedCaseIndex?: number) => void; +} + +const ArrayFileSubConditions: FC = ({ conditionFieldName, caseIndex, conditionIndex, name, filterNumberOptions, options, updateNodeLayout, updateNodePorts }) => { + const { t } = useTranslation(); + const form = Form.useFormInstance(); + const subValues = Form.useWatch([name, caseIndex, 'expressions', conditionIndex, 'sub_variable_condition', 'conditions'], form) + + const handleChangeSubLogicalOperator = () => { + const current = form.getFieldValue([name, caseIndex, 'expressions', conditionIndex, 'sub_variable_condition', 'logical_operator']) || 'and'; + form.setFieldValue([name, caseIndex, 'expressions', conditionIndex, 'sub_variable_condition', 'logical_operator'], current === 'and' ? 'or' : 'and'); + }; + const handleInputTypeChange = (caseIndex: number, conditionIndex: number, subIndex: number) => { + form.setFieldValue([name, caseIndex, 'expressions', conditionIndex, 'sub_variable_condition', 'conditions', subIndex, 'value'], undefined); + }; + + useEffect(() => { + console.log('subValues', subValues) + if (!subValues) return + const cases = form.getFieldValue(name) || []; + setTimeout(() => { + updateNodeLayout(cases); + const allSet = (subValues ?? []).every(isSubExprSet); + console.log('allSet', allSet) + updateNodePorts(cases.length); + }, 100); + }, [subValues]) + + return ( +
+ + {(subFields, { add: addSub, remove: removeSub }) => { + const subLogicalOperator = form.getFieldValue([name, caseIndex, 'expressions', conditionIndex, 'sub_variable_condition', 'logical_operator']) || 'and'; + return ( + <> +
1, + })}> + {subFields.length > 1 && ( +
+
+
+ + {subLogicalOperator} +
+
+
+
+
+ )} + {subFields.map((subField, subIndex) => { + const subExpr = form.getFieldValue([name, caseIndex, 'expressions', conditionIndex, 'sub_variable_condition', 'conditions', subIndex]) || {}; + const subOperator = subExpr.operator; + const subLeft = subExpr.key; + const subOperatorList = subLeft === 'type' ? fileSubFieldOperators.type : subLeft === 'size' ? fileSubFieldOperators.size : fileSubFieldOperators.default; + const hideSubRight = subOperator === 'empty' || subOperator === 'not_empty'; + const subInputType = subExpr.input_type + return ( + +
1, + 'rb:w-54.5': subFields.length === 1 + })}> + + + + ({ ...vo, label: t(String(vo?.label || '')) }))} + size="small" + popupMatchSelectWidth={false} + placeholder={t('common.pleaseSelect')} + variant="borderless" + className="rb:w-full!" + /> + + + + {!hideSubRight && ( +
+ {subLeft === 'size' + ? + + ({ value: vo, label: t(`application.${vo}`) }))} + variant="borderless" + className="rb:w-full!" + /> + : + } + + } +
+ )} +
+
{ removeSub(subField.name); }} + /> + + ); + })} +
+ + + ); + }} + +
+ ); +}; + const CaseList: FC = ({ options, name, @@ -251,7 +488,7 @@ const CaseList: FC = ({ left: newValue, operator: undefined, right: undefined, - input_type: undefined + input_type: 'constant' }); }; @@ -303,7 +540,7 @@ const CaseList: FC = ({ const logicalOperator = form.getFieldValue(name)?.[caseIndex]?.logical_operator || 'and' return ( - +
{caseIndex === 0 ? 'IF' : 'ELIF'}
{caseFields.length > 1 &&
{`CASE ${caseIndex + 1}`}
} @@ -314,9 +551,9 @@ const CaseList: FC = ({
- handleChangeLogicalOperator(caseIndex)}> + handleChangeLogicalOperator(caseIndex)}> {logicalOperator} -
+
@@ -333,7 +570,7 @@ const CaseList: FC = ({ ?? options.flatMap(o => o.children ?? []).find(child => `{{${child.value}}}` === leftFieldValue) ?? options.flatMap(o => o.children ?? []).flatMap((c: any) => c.children ?? []).find((gc: any) => `{{${gc.value}}}` === leftFieldValue); const leftFieldType = leftFieldOption?.dataType; - const hideRightField = currentOperator === 'empty' || currentOperator === 'not_empty' || leftFieldType === 'file' || leftFieldType === 'array[object]' || leftFieldType === 'array[file]'; + const hideRightField = currentOperator === 'empty' || currentOperator === 'not_empty' || leftFieldType === 'file' || leftFieldType === 'array[object]'; const operatorList = leftFieldType && operatorsObj[leftFieldType] ? operatorsObj[leftFieldType] : leftFieldType && leftFieldType?.includes('array') @@ -377,22 +614,40 @@ const CaseList: FC = ({
{!hideRightField && ( -
+
{leftFieldType === 'array[file]' - ? <>TODO + ? <> + + + + + : leftFieldType === 'number' ?