From 4a39fd5f464165299e1b88472ddb6ca194e4f3ed Mon Sep 17 00:00:00 2001 From: zhaoying Date: Mon, 20 Apr 2026 14:31:31 +0800 Subject: [PATCH 1/2] fix(web) if-else port y calculate update --- .../Workflow/components/CheckList/index.tsx | 8 +- .../components/Nodes/ConditionNode.tsx | 32 ++-- .../components/Properties/CaseList/index.tsx | 35 ++-- web/src/views/Workflow/constant.ts | 4 +- web/src/views/Workflow/utils.ts | 160 +++++------------- 5 files changed, 102 insertions(+), 137 deletions(-) diff --git a/web/src/views/Workflow/components/CheckList/index.tsx b/web/src/views/Workflow/components/CheckList/index.tsx index 4afdb863..b2598999 100644 --- a/web/src/views/Workflow/components/CheckList/index.tsx +++ b/web/src/views/Workflow/components/CheckList/index.tsx @@ -1,3 +1,9 @@ +/* + * @Author: ZhaoYing + * @Date: 2026-04-09 18:58:21 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-04-20 10:39:17 + */ import { useState, useCallback, useEffect, useRef, type FC } from 'react' import { Popover, Flex } from 'antd' import { WarningFilled } from '@ant-design/icons' @@ -49,7 +55,7 @@ const specialValidators: Record boolean> = { 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 !!expr.left && (expr?.sub_variable_condition || !!expr.right || typeof expr.right === 'boolean' || typeof expr.right === 'number') } return val.some(c => !c?.expressions?.length || c.expressions.some((expr: any) => !isExprSet(expr))) }, diff --git a/web/src/views/Workflow/components/Nodes/ConditionNode.tsx b/web/src/views/Workflow/components/Nodes/ConditionNode.tsx index 9cd1309e..1bd6559f 100644 --- a/web/src/views/Workflow/components/Nodes/ConditionNode.tsx +++ b/web/src/views/Workflow/components/Nodes/ConditionNode.tsx @@ -9,7 +9,7 @@ import { useVariableList } from '../Properties/hooks/useVariableList' import { isSubExprSet } from '../../utils' import { fileSubFieldOperators } from '../Properties/CaseList' -const caculateIsSet = (item: any, type: string) => { +const calculateIsSet = (item: any, type: string) => { switch (type) { case 'categories': return typeof item?.class_name === 'string' && item?.class_name !== '' @@ -79,7 +79,7 @@ const ConditionNode: ReactShapeConfig['component'] = ({ node }) => {
{t('workflow.config.question-classifier.class_name')} {index + 1} - {caculateIsSet(item, 'categories') ? t(`workflow.config.${data.type}.set`) : t(`workflow.config.${data.type}.unset`)} + {calculateIsSet(item, 'categories') ? t(`workflow.config.${data.type}.set`) : t(`workflow.config.${data.type}.unset`)}
))} @@ -89,17 +89,24 @@ const ConditionNode: ReactShapeConfig['component'] = ({ node }) => { {data.config?.cases?.defaultValue.map((item: any, index: number) => (
0 ? '' : 'rb:mb-1'}> - 0 ? "space-between" : 'end'} className="rb:mb-1"> - {item.expressions.length > 0 && CASE{index + 1}} + 0 ? "space-between" : 'end'} className="rb:mb-1! rb:leading-4"> + {item.expressions.length > 0 && CASE{index + 1}} {index === 0 ? 'IF' : `ELIF`} {item.expressions.length > 0 && {item.expressions.map((expression: any, eIndex: number) => (
- {item.expressions.length > 1 && eIndex > 0 &&
{item.logical_operator?.toLocaleUpperCase()}
} - + {item.expressions.length > 1 && eIndex > 0 && +
{item.logical_operator?.toLocaleUpperCase()}
+ } + 0, + 'rb:py-1!': !expression.sub_variable_condition?.conditions || !expression.sub_variable_condition?.conditions?.length + })} + > - {caculateIsSet(expression, 'cases') + {calculateIsSet(expression, 'cases') ? <> {labelRender(expression.left)} {getLocaleField(expression.operator, typeof expression.right)} @@ -109,11 +116,16 @@ const ConditionNode: ReactShapeConfig['component'] = ({ node }) => { } {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)} @@ -129,7 +141,7 @@ const ConditionNode: ReactShapeConfig['component'] = ({ node }) => { ))}
: 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 2fd24628..a9da1457 100644 --- a/web/src/views/Workflow/components/Properties/CaseList/index.tsx +++ b/web/src/views/Workflow/components/Properties/CaseList/index.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-09 18:24:53 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-04-17 20:47:49 + * @Last Modified time: 2026-04-20 10:46:05 */ import { useEffect, useMemo, type FC } from 'react' import clsx from 'clsx' @@ -39,7 +39,7 @@ interface Expression { sub_variable_condition?: SubVariableCondition; } -interface CaseItem { +export interface CaseItem { logical_operator: 'and' | 'or'; expressions: Expression[]; } @@ -274,7 +274,9 @@ const ArrayFileSubConditions: FC = ({ conditionFiel className="rb:w-full!" suffix="Byte" size="small" - onChange={(value) => { form.setFieldValue([name, caseIndex, 'expressions', conditionIndex, 'right'], value); }} + onChange={(value) => { + form.setFieldValue([name, caseIndex, 'expressions', conditionIndex, 'sub_variable_condition', 'conditions', subIndex, 'value'], value); + }} /> } @@ -483,13 +485,24 @@ const CaseList: FC = ({ form.setFieldValue([name, index, 'logical_operator'], currentValue === 'and' ? 'or' : 'and'); }; - const handleLeftFieldChange = (caseIndex: number, conditionIndex: number, newValue: string) => { - form.setFieldValue([name, caseIndex, 'expressions', conditionIndex], { - left: newValue, - operator: undefined, - right: undefined, - input_type: 'constant' - }); + const handleLeftFieldChange = (caseIndex: number, conditionIndex: number, newValue: string, option?: Suggestion | undefined) => { + if (option?.dataType === 'array[file]') { + form.setFieldValue([name, caseIndex, 'expressions', conditionIndex], { + left: newValue, + operator: undefined, + sub_variable_condition: { + conditions: [], + logical_operator: 'and' + } + }); + } else { + form.setFieldValue([name, caseIndex, 'expressions', conditionIndex], { + left: newValue, + operator: undefined, + right: undefined, + input_type: 'constant' + }); + } }; const handleAddCase = (addCaseFunc: Function) => { @@ -590,7 +603,7 @@ const CaseList: FC = ({ options={options} size="small" allowClear={false} - onChange={(val) => handleLeftFieldChange(caseIndex, conditionIndex, val as string)} + onChange={(val, option) => handleLeftFieldChange(caseIndex, conditionIndex, val as string, option as unknown as Suggestion)} variant="borderless" className="rb:w-36!" /> diff --git a/web/src/views/Workflow/constant.ts b/web/src/views/Workflow/constant.ts index cae20180..c11eb4c0 100644 --- a/web/src/views/Workflow/constant.ts +++ b/web/src/views/Workflow/constant.ts @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-03 15:06:18 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-04-16 17:52:30 + * @Last Modified time: 2026-04-20 11:39:40 */ import LoopNode from './components/Nodes/LoopNode'; import NormalNode from './components/Nodes/NormalNode'; @@ -734,7 +734,7 @@ export const portTextAttrs = { fontSize: 12, fill: '#5B6167' } /** * Port position arguments */ -export const portItemArgsY = 26.5; +export const portItemArgsY = 27.5; export const portArgs = { x: nodeWidth, y: portItemArgsY } const defaultPortGroup = { diff --git a/web/src/views/Workflow/utils.ts b/web/src/views/Workflow/utils.ts index bd81b6eb..74dfca2c 100644 --- a/web/src/views/Workflow/utils.ts +++ b/web/src/views/Workflow/utils.ts @@ -2,136 +2,70 @@ * @Author: ZhaoYing * @Date: 2026-03-24 15:07:49 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-04-17 20:40:47 + * @Last Modified time: 2026-04-20 14:20:34 */ -import { portItemArgsY, conditionNodePortItemArgsY, conditionNodeHeight } from './constant' +import { conditionNodePortItemArgsY, conditionNodeHeight } from './constant' -/** - * Calculate the total height of a condition (if-else) node based on its cases. - * - * The height is composed of: - * - `conditionNodeHeight`: the base height of the node (header + padding). - * - `(cases.length - 1) * 26`: vertical spacing added for each additional case - * beyond the first (each case separator row is 26px). - * - `exprCount * 20`: each individual expression row occupies 20px. - * - `hasMultiExprCount * 3`: a small extra padding (3px per expression) is added - * for cases that contain more than one expression, to account for the logical - * operator indicator (AND/OR) between expressions. - * - * @param cases - Array of case objects, each containing an `expressions` array. - * @returns The total pixel height for the condition node. - */ export const isSubExprSet = (sub: any) => { if (!sub?.key) return false; if (['not_empty', 'empty'].includes(sub?.operator)) return true; return !!sub.value || typeof sub.value === 'boolean' || typeof sub.value === 'number'; }; - -const getEffectiveExprCount = (expr: any): number => { - const subs = expr?.sub_variable_condition?.conditions; - if (subs?.length && subs.every(isSubExprSet)) return 1 + subs.length; - if (subs?.length > 0) { - return 2 - } - return 1; +/** + * Calculate the total height of a condition (if-else) node based on its cases. + * Uses the same per-expression height logic as getConditionNodeCasePortY. + */ +export const calcConditionNodeTotalHeight = (cases: any[]) => { + const casesHeight = cases.reduce((acc: number, c: any) => { + const exprs = c?.expressions ?? []; + const n = exprs.length; + const exprsHeight = n === 0 ? 0 : exprs.reduce((s: number, e: any) => s + calcExpressionHeight(e), 0) + 2 * (n - 1); + return acc + 20 + exprsHeight; + }, 0); + return conditionNodeHeight + casesHeight + (cases.length - 1) * 4 - 27.5; }; -export const calcConditionNodeTotalHeight = (cases: any[]) => { - // Total number of effective expression rows (sub_variable_condition expand height when all set) - const exprCount = cases.reduce((acc: number, c: any) => - acc + (c?.expressions?.reduce((s: number, e: any) => s + getEffectiveExprCount(e), 0) || 0), 0); - // Sum of effective expression counts only for cases that have more than one expression - const hasMultiExprCount = cases.reduce((acc: number, c: any) => { - if (!c?.expressions?.length || c.expressions.length <= 1) return acc; - const effectiveCount = c.expressions.reduce((s: number, e: any) => s + getEffectiveExprCount(e), 0); - return acc + effectiveCount; - }, 0); - - return conditionNodeHeight + (cases.length - 1) * 26 + exprCount * 20 + hasMultiExprCount * 3; +/** + * Height of a single expression block in ConditionNode (px). + * + * expression outer Flex padding: + * - has sub conditions (length > 0): pt-1 (4px top only) + * - no sub conditions: py-1 (4px top + 4px bottom) + * expression main row: leading-4 = 16px + * sub_variable_condition block (mt-1 = 4px gap): + * - all isSet, m subs: sub[0] = leading-3.5(14) + pb-1(4) = 18px; + * sub[k>0] = py-1(8) + leading-3.5(14) = 22px + * total = 18 + 22*(m-1) + * - exists but not all isSet: pb-1(4) + leading-4(16) = 20px + */ +const calcExpressionHeight = (expression: any): number => { + const subs = expression?.sub_variable_condition?.conditions; + if (!subs?.length) return 24; // py-1(8) + leading-4(16) + const subBlockHeight = subs.every(isSubExprSet) + ? 18 + 22 * (subs.length - 1) + : 20; + return 4 + 16 + 4 + subBlockHeight - 2; // pt-1 + main row + mt-1 + sub block (-2 rendering correction) }; /** * Calculate the Y-coordinate of the right-side output port for a specific case - * in a condition (if-else) node. + * in a condition (if-else) node, aligned with the IF/ELIF label in ConditionNode. * - * The port position is determined by iterating through all preceding cases - * (index 0 to caseIndex - 1) and accumulating their visual heights. Several - * pixel-level corrections are applied to align ports with the rendered UI: - * - * 1. **Base offset**: starts at `conditionNodePortItemArgsY`, which is the Y - * position of the first case port relative to the node top. - * - * 2. **Per-case accumulation**: for each preceding case with `n` expressions, - * add `portItemArgsY * (n + 1)` — this accounts for `n` expression rows - * plus one case header/separator row. - * - * 3. **Single-expression correction**: cases with exactly 1 expression render - * slightly shorter than the generic formula predicts. Subtract - * `singleExprCount * 7 + 2` to compensate for the reduced row height when - * no logical operator row is shown. - * - * 4. **Multi-expression correction**: cases with 2+ expressions have a compact - * logical operator row. Subtract `multiExprCount * 9` to offset the - * over-estimated spacing. - * - * 5. **Extra expression correction**: for cases with more than 2 expressions, - * each additional expression beyond the second introduces a minor spacing - * discrepancy. Subtract `(extraExprs + 1) * 2` to fine-tune alignment. - * - * @param cases - Array of case objects, each containing an `expressions` array. - * @param caseIndex - The zero-based index of the target case whose port Y is needed. - * @returns The Y-coordinate (in pixels) for the output port of the given case. + * Layout (from node top): + * - 12px padding-top + 24px header + 12px mt-3 = 48px to cases area + * - Each IF/ELIF label row: leading-4 (16px), center at +8px → first port Y = 56.5 + * - Each case: IF/ELIF row (leading-4=16) + mb-1(4) + expressions (gap={2}=2px between) + * - Gap between cases (Flex gap={4}): 4px */ export const getConditionNodeCasePortY = (cases: any[], caseIndex: number) => { - let y = conditionNodePortItemArgsY; - let singleExprCount = 0; - let multiExprCount = 0; - let extraExprs = 0; - let portItemArgsYNum = 0; - + let y = conditionNodePortItemArgsY; // 56.5, center of first IF label for (let i = 0; i < caseIndex; i++) { - const notHasSub = cases[i]?.expressions?.filter((e: any) => !e?.sub_variable_condition?.conditions || e?.sub_variable_condition?.conditions.length <1).length - const n = cases[i]?.expressions?.length || 0; - let casePortItemArgsYNum = n + 1; - // Add extra y for expressions with all sub_variable_condition set - cases[i]?.expressions?.forEach((expr: any) => { - const subs = expr?.sub_variable_condition?.conditions; - if (subs?.length && subs.every(isSubExprSet)) { - casePortItemArgsYNum += subs.length; - } else if (subs?.length) { - casePortItemArgsYNum += 1 - } - }); - portItemArgsYNum += casePortItemArgsYNum; - if (n === 1 && !cases[i]?.expressions?.some((e: any) => e?.sub_variable_condition?.conditions?.length > 0)) { - singleExprCount++ - } else if (n >= 2 || cases[i]?.expressions?.some((e: any) => e?.sub_variable_condition?.conditions?.length > 0)) { - multiExprCount++; - cases[i]?.expressions?.forEach((e: any) => { - const subs = e?.sub_variable_condition?.conditions; - if (subs?.length && subs.every(isSubExprSet) && subs.length > 1) { - extraExprs += subs.length + 2; - } - }); - - console.log('extraExprs notHasSub', notHasSub) - if (notHasSub > 3) { - extraExprs += n - 2 + notHasSub/4; - } else { - extraExprs += n - 2 + notHasSub/4 - } - } + const exprs = cases[i]?.expressions ?? []; + const n = exprs.length; + // IF/ELIF row (16) + mb-1 (4) = 20px base; expressions: sum of heights + 2px gap between + const exprsHeight = n === 0 ? 0 : exprs.reduce((acc: number, e: any) => acc + calcExpressionHeight(e), 0) + 2 * (n - 1); + y += 20 + exprsHeight + 4; // case height + Flex gap between cases } - - console.log('singleExprCount', singleExprCount, 'multiExprCount', multiExprCount, 'extraExprs', extraExprs) - y += portItemArgsY * portItemArgsYNum - // Correction for single-expression cases (slightly shorter rendered height) - if (singleExprCount > 0) y -= singleExprCount * 7 + 2; - // Correction for multi-expression cases (compact logical operator row) - y -= multiExprCount * 9; - // Correction for cases with more than 2 expressions (minor spacing drift) - if (extraExprs > 0) y -= (extraExprs + 1) * 2; - return y; -}; +}; \ No newline at end of file From 559b4bef6b362cd6bdfa82efa60e48d0a11145ba Mon Sep 17 00:00:00 2001 From: zhaoying Date: Mon, 20 Apr 2026 14:47:16 +0800 Subject: [PATCH 2/2] fix(web): add tool_id required check list --- web/src/i18n/en.ts | 1 + web/src/i18n/zh.ts | 1 + .../Workflow/components/Properties/ToolConfig/index.tsx | 5 +++++ web/src/views/Workflow/constant.ts | 5 +++-- 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index dfc42973..9470e2e9 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -2564,6 +2564,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re 'jinja-render.template': 'Template', 'document-extractor.file_selector': 'File variable', 'list-operator.input_list': 'Input list', + 'tool.tool_id': 'Tool', }, checkListHasErrors: 'Please resolve all issues in the checklist before publishing', variableSelect: { diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index ae0181c9..8a134a06 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -2528,6 +2528,7 @@ export const zh = { 'jinja-render.template': '模板', 'document-extractor.file_selector': '文件变量', 'list-operator.input_list': '输入变量', + 'tool.tool_id': '工具', }, checkListHasErrors: '发布前确认检查清单中所有问题均已解决', variableSelect: { diff --git a/web/src/views/Workflow/components/Properties/ToolConfig/index.tsx b/web/src/views/Workflow/components/Properties/ToolConfig/index.tsx index ce30ee8f..d38265da 100644 --- a/web/src/views/Workflow/components/Properties/ToolConfig/index.tsx +++ b/web/src/views/Workflow/components/Properties/ToolConfig/index.tsx @@ -147,6 +147,11 @@ const ToolConfig: FC<{ options: Suggestion[]; }> = ({ }; const handleChange: CascaderProps