= ({
>
{t('common.remove')}
-
+ }
diff --git a/web/src/views/Workflow/hooks/useWorkflowGraph.ts b/web/src/views/Workflow/hooks/useWorkflowGraph.ts
index 86cb91d5..197e4e4b 100644
--- a/web/src/views/Workflow/hooks/useWorkflowGraph.ts
+++ b/web/src/views/Workflow/hooks/useWorkflowGraph.ts
@@ -2,7 +2,7 @@
* @Author: ZhaoYing
* @Date: 2026-02-03 15:17:48
* @Last Modified by: ZhaoYing
- * @Last Modified time: 2026-03-20 11:26:43
+ * @Last Modified time: 2026-03-24 15:01:52
*/
import { useRef, useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
@@ -17,6 +17,7 @@ import type { WorkflowConfig, NodeProperties, ChatVariable } from '../types';
import { getWorkflowConfig, saveWorkflowConfig } from '@/api/application'
import { useUser } from '@/store/user';
import type { FeaturesConfigForm } from '@/views/ApplicationConfig/types'
+import { calcConditionNodeTotalHeight, getConditionNodeCasePortY } from '../utils'
/**
* Props for useWorkflowGraph hook
@@ -218,7 +219,6 @@ export const useWorkflowGraph = ({
// Generate ports dynamically for if-else node based on cases
if (type === 'if-else' && config.cases && Array.isArray(config.cases)) {
const totalPorts = config.cases.length + 1; // IF/ELIF + ELSE
- const newHeight = conditionNodeHeight + (totalPorts - 2) * conditionNodeItemHeight;
const portItems: PortMetadata[] = [
defaultPortItems[0],
@@ -230,7 +230,7 @@ export const useWorkflowGraph = ({
id: `CASE${i + 1}`,
args: {
x: nodeWidth,
- y: portItemArgsY * i + conditionNodePortItemArgsY,
+ y: getConditionNodeCasePortY(config.cases, i),
},
});
}
@@ -240,7 +240,7 @@ export const useWorkflowGraph = ({
items: portItems
};
- nodeConfig.height = newHeight;
+ nodeConfig.height = calcConditionNodeTotalHeight(config.cases);
}
// Generate ports dynamically for question-classifier node based on categories
diff --git a/web/src/views/Workflow/utils.ts b/web/src/views/Workflow/utils.ts
new file mode 100644
index 00000000..67a913f3
--- /dev/null
+++ b/web/src/views/Workflow/utils.ts
@@ -0,0 +1,90 @@
+/*
+ * @Author: ZhaoYing
+ * @Date: 2026-03-24 15:07:49
+ * @Last Modified by: ZhaoYing
+ * @Last Modified time: 2026-03-24 15:07:49
+ */
+
+import { portItemArgsY, 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 calcConditionNodeTotalHeight = (cases: any[]) => {
+ // Total number of expressions across all cases
+ const exprCount = cases.reduce((acc: number, c: any) => acc + (c?.expressions?.length || 0), 0);
+ // Sum of expression counts only for cases that have more than one expression
+ const hasMultiExprCount = cases.reduce((acc: number, c: any) => acc + (c?.expressions?.length > 1 ? c?.expressions?.length : 0), 0);
+
+ return conditionNodeHeight + (cases.length - 1) * 26 + exprCount * 20 + hasMultiExprCount * 3;
+};
+
+/**
+ * Calculate the Y-coordinate of the right-side output port for a specific case
+ * in a condition (if-else) node.
+ *
+ * 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.
+ */
+export const getConditionNodeCasePortY = (cases: any[], caseIndex: number) => {
+ let y = conditionNodePortItemArgsY;
+ let singleExprCount = 0;
+ let multiExprCount = 0;
+ let extraExprs = 0;
+
+ for (let i = 0; i < caseIndex; i++) {
+ const n = cases[i]?.expressions?.length || 0;
+ y += portItemArgsY * (n + 1);
+ if (n === 1) singleExprCount++;
+ else if (n >= 2) {
+ multiExprCount++;
+ if (n > 2) extraExprs += n - 2;
+ }
+ }
+
+ // 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;
+};