feat(web): workflow ui upgrade
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { useState, useImperativeHandle, forwardRef, useRef } from 'react';
|
||||
import { Button, Space, List } from 'antd';
|
||||
import { Button, List, Flex } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { ChatVariable, AddChatVariableRef } from '../../types';
|
||||
import type { ChatVariableModalRef } from './types'
|
||||
import RbDrawer from '@/components/RbDrawer';
|
||||
@@ -74,15 +75,15 @@ const AddChatVariable = forwardRef<AddChatVariableRef, AddChatVariableProps>(({
|
||||
dataSource={variables}
|
||||
renderItem={(item, index) => (
|
||||
<List.Item>
|
||||
<div key={index} className="rb:relative rb:p-[12px_16px] rb:bg-[#FBFDFF] rb:cursor-pointer rb:border rb:border-[#DFE4ED] rb:rounded-lg">
|
||||
<div className="rb:flex rb:items-center rb:justify-between">
|
||||
<div key={index} className="rb:relative rb:p-[12px_16px] rb:bg-[#FBFDFF] rb:cursor-pointer rb-border rb:rounded-lg">
|
||||
<Flex align="center" justify="space-between">
|
||||
<div className="rb:leading-4">
|
||||
<span className="rb:font-medium">{item.name}</span>
|
||||
<span className="rb:text-[12px] rb:text-[#5B6167] rb:font-regular"> ({t(`workflow.config.parameter-extractor.${item.type}`)})</span>
|
||||
</div>
|
||||
</div>
|
||||
</Flex>
|
||||
<div className="rb:mt-1 rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-5 rb:wrap-break-word rb:line-clamp-1">{item.description}</div>
|
||||
<Space size={12} className="rb:flex rb:absolute rb:right-4 rb:top-[50%] rb:transform-[translateY(-50%)] rb:bg-white">
|
||||
<Flex gap={12} className="rb:absolute rb:right-4 rb:top-[50%] rb:transform-[translateY(-50%)] rb:bg-white">
|
||||
<div
|
||||
className="rb:size-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/editBorder.svg')] rb:hover:bg-[url('@/assets/images/editBg.svg')]"
|
||||
onClick={() => handleEdit(index)}
|
||||
@@ -91,7 +92,7 @@ const AddChatVariable = forwardRef<AddChatVariableRef, AddChatVariableProps>(({
|
||||
className="rb:size-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/deleteBorder.svg')] rb:hover:bg-[url('@/assets/images/deleteBg.svg')]"
|
||||
onClick={() => handleDelete(index)}
|
||||
></div>
|
||||
</Space>
|
||||
</Flex>
|
||||
</div>
|
||||
</List.Item>
|
||||
)}
|
||||
|
||||
@@ -2,7 +2,6 @@ import type { FC } from 'react';
|
||||
import { Select } from 'antd';
|
||||
// import { Node } from '@antv/x6';
|
||||
import type { GraphRef } from '../types'
|
||||
import { PlusOutlined, MinusOutlined } from '@ant-design/icons'
|
||||
|
||||
interface CanvasToolbarProps {
|
||||
miniMapRef: React.RefObject<HTMLDivElement>;
|
||||
@@ -10,150 +9,20 @@ interface CanvasToolbarProps {
|
||||
isHandMode: boolean;
|
||||
setIsHandMode: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
zoomLevel: number;
|
||||
canUndo: boolean;
|
||||
canRedo: boolean;
|
||||
onUndo: () => void;
|
||||
onRedo: () => void;
|
||||
}
|
||||
|
||||
const CanvasToolbar: FC<CanvasToolbarProps> = ({
|
||||
miniMapRef,
|
||||
graphRef,
|
||||
// isHandMode,
|
||||
// setIsHandMode,
|
||||
zoomLevel,
|
||||
// canUndo,
|
||||
// canRedo,
|
||||
// onUndo,
|
||||
// onRedo,
|
||||
}) => {
|
||||
// 整理布局函数
|
||||
/*
|
||||
const handleLayout = () => {
|
||||
if (!graphRef.current) return;
|
||||
const nodes = graphRef.current.getNodes();
|
||||
const edges = graphRef.current.getEdges();
|
||||
|
||||
// 如果没有连线,使用垂直布局避免节点重叠
|
||||
if (edges.length === 0) {
|
||||
nodes.forEach((node, index) => {
|
||||
const nodeData = node.getData();
|
||||
const isSpecialNode = nodeData?.isGroup || nodeData?.type === 'if-else';
|
||||
const nodeHeight = isSpecialNode ? 220 : 50;
|
||||
const xPosition = 100;
|
||||
const yPosition = index * (nodeHeight + 100) + 100;
|
||||
node.setPosition(xPosition, yPosition);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 简单的树布局算法
|
||||
const nodeMap = new Map<string, Node>();
|
||||
const children = new Map<string, string[]>();
|
||||
const roots: string[] = [];
|
||||
|
||||
// 初始化节点映射
|
||||
nodes.forEach(node => {
|
||||
nodeMap.set(node.id, node);
|
||||
children.set(node.id, []);
|
||||
});
|
||||
|
||||
// 构建父子关系
|
||||
edges.forEach(edge => {
|
||||
const sourceId = edge.getSourceCellId();
|
||||
const targetId = edge.getTargetCellId();
|
||||
if (sourceId && targetId) {
|
||||
children.get(sourceId)?.push(targetId);
|
||||
}
|
||||
});
|
||||
|
||||
// 找到根节点
|
||||
const hasParent = new Set<string>();
|
||||
edges.forEach(edge => {
|
||||
const targetId = edge.getTargetCellId();
|
||||
if (targetId) hasParent.add(targetId);
|
||||
});
|
||||
|
||||
nodes.forEach(node => {
|
||||
if (!hasParent.has(node.id)) {
|
||||
roots.push(node.id);
|
||||
}
|
||||
});
|
||||
|
||||
// 布局参数
|
||||
const levelWidths: number[] = [];
|
||||
const baseNodeSpacing = 120;
|
||||
let currentY = 100;
|
||||
|
||||
// 计算每层的最大宽度
|
||||
const calculateLevelWidths = (nodeId: string, level: number) => {
|
||||
const node = nodeMap.get(nodeId);
|
||||
if (!node) return;
|
||||
|
||||
const nodeData = node.getData();
|
||||
const isSpecialNode = nodeData?.isGroup || nodeData?.type === 'if-else';
|
||||
const nodeWidth = isSpecialNode ? 400 : 160;
|
||||
const gap = isSpecialNode ? 150 : 100;
|
||||
|
||||
levelWidths[level] = Math.max(levelWidths[level] || 0, nodeWidth + gap);
|
||||
|
||||
const childIds = children.get(nodeId) || [];
|
||||
childIds.forEach((childId: string) => calculateLevelWidths(childId, level + 1));
|
||||
};
|
||||
|
||||
roots.forEach(rootId => calculateLevelWidths(rootId, 0));
|
||||
|
||||
// 递归布局函数
|
||||
const layoutNode = (nodeId: string, level: number, parentY: number): number => {
|
||||
const node = nodeMap.get(nodeId);
|
||||
if (!node) return parentY;
|
||||
|
||||
const nodeData = node.getData();
|
||||
const isSpecialNode = nodeData?.isGroup || nodeData?.type === 'if-else';
|
||||
const nodeHeight = isSpecialNode ? 220 : 50;
|
||||
const verticalGap = isSpecialNode ? 80 : 40;
|
||||
const spacing = baseNodeSpacing + nodeHeight + verticalGap;
|
||||
|
||||
const xPosition = levelWidths.slice(0, level).reduce((sum, width) => sum + width, 100);
|
||||
|
||||
const childIds = children.get(nodeId) || [];
|
||||
|
||||
if (childIds.length === 0) {
|
||||
// 叶子节点
|
||||
node.setPosition(xPosition, currentY);
|
||||
currentY += spacing;
|
||||
return currentY - spacing;
|
||||
} else {
|
||||
// 非叶子节点,先布局子节点
|
||||
const childPositions: number[] = [];
|
||||
childIds.forEach((childId: string) => {
|
||||
const childY = layoutNode(childId, level + 1, currentY);
|
||||
childPositions.push(childY);
|
||||
});
|
||||
|
||||
// 父节点居中,确保有足够间隙
|
||||
const minY = Math.min(...childPositions);
|
||||
const maxY = Math.max(...childPositions);
|
||||
const centerY = (minY + maxY) / 2;
|
||||
node.setPosition(xPosition, centerY);
|
||||
return centerY;
|
||||
}
|
||||
};
|
||||
|
||||
// 布局所有根节点
|
||||
roots.forEach(rootId => {
|
||||
layoutNode(rootId, 0, currentY);
|
||||
currentY += 300; // 不同树之间的间距
|
||||
});
|
||||
};
|
||||
*/
|
||||
return (
|
||||
<>
|
||||
{/* 小地图 */}
|
||||
<div ref={miniMapRef} className="rb:absolute rb:bottom-15 rb:right-8 rb:z-1000 rb:rounded-lg rb:overflow-hidden"></div>
|
||||
{/* 缩放控制按钮 */}
|
||||
<div className="rb:h-8.5 rb:bg-[#FFFFFF] rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:shadow-[0px_2px_6px_0px_rgba(33,35,50,0.15)] rb:px-3 rb:py-2 rb:absolute rb:bottom-5 rb:right-8 rb:flex rb:flex-row rb:gap-4 rb:z-1000">
|
||||
<MinusOutlined className="rb:text-[16px] rb:cursor-pointer" onClick={() => graphRef.current?.zoom(-0.1)} />
|
||||
<div className="rb:h-8.5 rb:bg-[#FFFFFF] rb-border rb:rounded-lg rb:shadow-[0px_2px_6px_0px_rgba(33,35,50,0.15)] rb:px-3 rb:py-2.25 rb:absolute rb:bottom-5 rb:right-8 rb:flex rb:flex-row rb:gap-2 rb:z-1000">
|
||||
<div className="rb:size-4 rb:bg-cover rb:bg-[url('@/assets/images/workflow/minus.png')]" onClick={() => graphRef.current?.zoom(-0.1)}></div>
|
||||
<Select
|
||||
value={Math.round(zoomLevel * 100)}
|
||||
onChange={(value: number | string) => {
|
||||
@@ -181,7 +50,7 @@ const CanvasToolbar: FC<CanvasToolbarProps> = ({
|
||||
variant='borderless'
|
||||
size="small"
|
||||
/>
|
||||
<PlusOutlined className="rb:text-[16px] rb:cursor-pointer" onClick={() => graphRef.current?.zoom(0.1)} />
|
||||
<div className="rb:size-4 rb:bg-cover rb:bg-[url('@/assets/images/workflow/plus.png')]" onClick={() => graphRef.current?.zoom(0.1)}></div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -433,12 +433,12 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef }>(({ appId
|
||||
|
||||
return (
|
||||
<RbDrawer
|
||||
title={<div className="rb:flex rb:items-center rb:gap-2.5">
|
||||
title={<Flex align="center" gap={10}>
|
||||
{t('workflow.run')}
|
||||
{variables.length > 0 && <Space>
|
||||
<Button size="small" onClick={handleEditVariables}>{t('application.variable')}</Button>
|
||||
</Space>}
|
||||
</div>}
|
||||
</Flex>}
|
||||
classNames={{
|
||||
body: 'rb:p-0!'
|
||||
}}
|
||||
@@ -458,7 +458,7 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef }>(({ appId
|
||||
return <Runtime item={item} index={index} />
|
||||
}}
|
||||
/>
|
||||
<div className="rb:relative rb:flex rb:items-center rb:gap-2.5 rb:m-4 rb:mb-1">
|
||||
<Flex align="center" gap={10} className="rb:relative rb:m-4! rb:mb-1!">
|
||||
<ChatInput
|
||||
message={message}
|
||||
className="rb:relative!"
|
||||
@@ -496,7 +496,7 @@ const Chat = forwardRef<ChatRef, { appId: string; graphRef: GraphRef }>(({ appId
|
||||
</Flex>
|
||||
</Flex>
|
||||
</ChatInput>
|
||||
</div>
|
||||
</Flex>
|
||||
|
||||
<VariableConfigModal
|
||||
ref={variableConfigModalRef}
|
||||
|
||||
@@ -28,6 +28,9 @@
|
||||
background-color: transparent;
|
||||
border-top: none;
|
||||
}
|
||||
:global(.ant-collapse .ant-collapse-content>.ant-collapse-content-box) {
|
||||
padding-top: 0;
|
||||
}
|
||||
.collapse-item :global(.ant-collapse) {
|
||||
/* background-color: #F0F3F8; */
|
||||
background-color: #FBFDFF;
|
||||
@@ -38,5 +41,5 @@
|
||||
border-radius: 0 0 6px 6px;
|
||||
}
|
||||
.collapse-item :global(.ant-collapse .ant-collapse-content>.ant-collapse-content-box) {
|
||||
padding: 4px;
|
||||
padding: 0 4px 4px 4px;
|
||||
}
|
||||
@@ -36,7 +36,7 @@ export interface LexicalEditorProps {
|
||||
fontSize?: number;
|
||||
lineHeight?: number;
|
||||
size?: 'default' | 'small';
|
||||
type?: 'input' | 'textarea',
|
||||
type?: 'input' | 'textarea';
|
||||
language?: 'string' | 'jinja2';
|
||||
className?: string;
|
||||
}
|
||||
@@ -61,7 +61,7 @@ const jinja2Theme = {
|
||||
};
|
||||
|
||||
// Main Lexical Editor component
|
||||
const Editor: FC<LexicalEditorProps> =(({
|
||||
const Editor: FC<LexicalEditorProps> =({
|
||||
placeholder = "请输入内容...",
|
||||
value = "",
|
||||
onChange,
|
||||
@@ -173,10 +173,10 @@ const Editor: FC<LexicalEditorProps> =(({
|
||||
// Calculate minimum height based on type and size
|
||||
const minheight = useMemo(() => {
|
||||
if (type === 'input') {
|
||||
return `${height ? height : size === 'small' ? 28 : 30}px`
|
||||
return `${height ? height : size === 'small' && variant === 'borderless' ? 18 : size === 'small' ? 26 : 30}px`
|
||||
}
|
||||
return `${height ? height : size === 'small' ? 60 : 120}px`
|
||||
}, [type, size, height])
|
||||
}, [type, size, height, variant])
|
||||
|
||||
// Calculate font size based on size prop
|
||||
const fontSize = useMemo(() => {
|
||||
@@ -185,12 +185,12 @@ const Editor: FC<LexicalEditorProps> =(({
|
||||
|
||||
// Calculate line height based on size prop
|
||||
const lineHeight = useMemo(() => {
|
||||
return `${height ? height : size === 'small' ? 16 : 20}px`
|
||||
return `${height ? 16 : size === 'small' && variant === 'borderless' ? 18 : size === 'small' ? 16 : 20}px`
|
||||
}, [size])
|
||||
|
||||
// Calculate placeholder minimum height
|
||||
const placeHolderMinheight = useMemo(() => {
|
||||
return `${height ? height : size === 'small' ? 16 : 30}px`
|
||||
return `${height ? 16 : size === 'small' ? 16 : 30}px`
|
||||
}, [type, size, height])
|
||||
|
||||
return (
|
||||
@@ -213,7 +213,7 @@ const Editor: FC<LexicalEditorProps> =(({
|
||||
className="editor-content-with-numbers"
|
||||
style={{
|
||||
minHeight: minheight,
|
||||
padding: '4px 0',
|
||||
padding: variant === 'borderless' ? '0' : '4px 0',
|
||||
outline: 'none',
|
||||
resize: 'none',
|
||||
fontSize: fontSize,
|
||||
@@ -228,9 +228,9 @@ const Editor: FC<LexicalEditorProps> =(({
|
||||
<ContentEditable
|
||||
style={{
|
||||
minHeight: minheight,
|
||||
padding: variant === 'borderless' ? '0' : '4px 11px',
|
||||
border: variant === 'borderless' ? 'none' : '1px solid #DFE4ED',
|
||||
borderRadius: '6px',
|
||||
padding: height || variant === 'borderless' ? '0' : '6px 8px',
|
||||
border: variant === 'borderless' ? 'none' : '1px solid #EBEBEB',
|
||||
borderRadius: '8px',
|
||||
outline: 'none',
|
||||
resize: 'none',
|
||||
fontSize: fontSize,
|
||||
@@ -269,6 +269,6 @@ const Editor: FC<LexicalEditorProps> =(({
|
||||
</div>
|
||||
</LexicalComposer>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export default Editor;
|
||||
|
||||
@@ -37,7 +37,7 @@ const VariableComponent: React.FC<{ nodeKey: NodeKey; data: Suggestion }> = ({
|
||||
<span
|
||||
onClick={handleClick}
|
||||
className={clsx('rb:border rb:rounded-md rb:bg-white rb:text-[10px] rb:inline-flex rb:items-center rb:py-0 rb:px-1.5 rb:mx-0.5 rb:cursor-pointer', {
|
||||
'rb:border-[#155EEF]': isSelected,
|
||||
'rb:border-[#171719]': isSelected,
|
||||
'rb:border-[#DFE4ED]': !isSelected
|
||||
})}
|
||||
contentEditable={false}
|
||||
@@ -57,7 +57,7 @@ const VariableComponent: React.FC<{ nodeKey: NodeKey; data: Suggestion }> = ({
|
||||
<span style={{ color: '#DFE4ED', margin: '0 2px' }}>/</span>
|
||||
</>
|
||||
)}
|
||||
<span className="rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap rb:flex-1" style={{ color: '#155EEF' }}>{data.label}</span>
|
||||
<span className="rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap rb:flex-1 rb:text-[#171719]">{data.label}</span>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -49,7 +49,7 @@ const Jinja2HighlightPlugin = () => {
|
||||
newNode.setStyle('color: #008000');
|
||||
break;
|
||||
case 'brace-0':
|
||||
newNode.setStyle('color: #155EEF; font-family: monospace; font-weight: bold;');
|
||||
newNode.setStyle('color: #171719; font-family: monospace; font-weight: bold;');
|
||||
break;
|
||||
case 'brace-1':
|
||||
newNode.setStyle('color: #369F21; font-family: monospace; font-weight: bold;');
|
||||
|
||||
@@ -1,48 +1,94 @@
|
||||
import { type FC } from 'react';
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Card, Space } from 'antd'
|
||||
import { Flex, Tooltip } from 'antd'
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { nodeLibrary } from '../constant';
|
||||
import RbCard from '@/components/RbCard/Card';
|
||||
|
||||
const NodeLibrary: FC = () => {
|
||||
const NodeLibrary: FC<{ collapsed: boolean; handleToggle: () => void }> = ({ collapsed, handleToggle }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
console.log('nodeLibrary', nodeLibrary)
|
||||
|
||||
return (
|
||||
<div className="rb:w-80 rb:fixed rb:h-[calc(100%-64px)] rb:left-0 rb:py-5 rb:px-5.5 rb:overflow-y-auto">
|
||||
<Space size={12} direction="vertical" className="rb:w-full">
|
||||
{nodeLibrary.map(category => (
|
||||
<Card
|
||||
key={category.category}
|
||||
type="inner"
|
||||
title={t(`workflow.${category.category}`)}
|
||||
classNames={{
|
||||
body: "rb:p-[10px]!",
|
||||
header: "rb:bg-[#F6F8FC]!"
|
||||
}}
|
||||
<div className={clsx("rb:overflow-hidden rb:fixed rb:left-2.5 rb:top-18.5 rb:z-1000", {
|
||||
'rb:w-65': !collapsed,
|
||||
'rb:w-14': collapsed
|
||||
})}>
|
||||
<RbCard
|
||||
title={collapsed ? undefined :t('workflow.nodeName')}
|
||||
extra={
|
||||
<div className={clsx("rb:cursor-pointer rb:size-5 rb:bg-cover rb:bg-[url('@/assets/images/workflow/menuFold.svg')]", {
|
||||
'rb:rotate-180 rb:mr-1': collapsed
|
||||
})} onClick={handleToggle}></div>
|
||||
}
|
||||
headerType="borderless"
|
||||
headerClassName={clsx("rb:font-[MiSans-Bold] rb:font-bold rb:text-[12px]!", {
|
||||
'rb:min-h-[42px]!': !collapsed,
|
||||
'rb:min-h-[52px]!': collapsed
|
||||
})}
|
||||
className="rb:h-full! rb:hover:shadow-none!"
|
||||
bodyClassName={clsx('rb:overflow-y-auto! rb:h-[calc(100vh-126px)]! rb:pt-0! rb:pb-3!', {
|
||||
'rb:px-0!': collapsed,
|
||||
'rb:px-3!': !collapsed
|
||||
})}
|
||||
>
|
||||
<Space size={8} direction="vertical" className="rb:w-full">
|
||||
<Flex vertical align={collapsed ? 'center' : undefined} gap={collapsed ? 8 : 16}>
|
||||
{collapsed
|
||||
? <>
|
||||
{nodeLibrary.map(category => (
|
||||
<>
|
||||
{category.nodes
|
||||
.filter(node => node.type !== 'cycle-start' && node.type !== 'break')
|
||||
.map((node, nodeIndex) => (
|
||||
<Tooltip key={nodeIndex} title={t(`workflow.${node.type}`)} placement="right">
|
||||
<div
|
||||
key={nodeIndex}
|
||||
className="rb:bg-white rb:rounded-lg rb:p-2 rb:border rb:border-[#DFE4ED] rb:cursor-pointer rb:flex rb:items-center rb:gap-2 rb:hover:border-[#155EEF] rb:hover:shadow-[0px_2px_4px_0px_rgba(33,35,50,0.15)]"
|
||||
className="rb:p-2 rb:rounded-lg rb:hover:bg-[rgba(33,35,50,0.08)]"
|
||||
draggable
|
||||
onDragStart={(e) => {
|
||||
e.dataTransfer.setData('application/reactflow', node.type);
|
||||
e.dataTransfer.setData('application/json', JSON.stringify(node));
|
||||
}}
|
||||
>
|
||||
<img src={node.icon} className="rb:w-5 rb:h-5" />
|
||||
<span className="rb:font-medium rb:text-[12px]">{t(`workflow.${node.type}`)}</span>
|
||||
<img src={node.icon} className="rb:size-6 rb:cursor-pointer" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
))
|
||||
}
|
||||
</>
|
||||
))}
|
||||
</Space>
|
||||
</Card>
|
||||
</>
|
||||
: nodeLibrary.map(category => (
|
||||
<div
|
||||
key={category.category}
|
||||
>
|
||||
<div className="rb:font-semibold rb:mb-2 rb:text-[12px] rb:leading-4.5 rb:pl-1">{t(`workflow.${category.category}`)}</div>
|
||||
<Flex gap={6} vertical>
|
||||
{category.nodes
|
||||
.filter(node => node.type !== 'cycle-start' && node.type !== 'break')
|
||||
.map((node, nodeIndex) => (
|
||||
<Flex
|
||||
key={nodeIndex}
|
||||
align="center"
|
||||
gap={8}
|
||||
className="rb:rounded-xl rb:p-2! rb-border rb:cursor-pointer rb:hover:border rb:hover:border-[#171719]!"
|
||||
draggable
|
||||
onDragStart={(e) => {
|
||||
e.dataTransfer.setData('application/reactflow', node.type);
|
||||
e.dataTransfer.setData('application/json', JSON.stringify(node));
|
||||
}}
|
||||
>
|
||||
<img src={node.icon} className="rb:size-6" />
|
||||
<span className="rb:font-medium rb:text-[12px] rb:leading-4">{t(`workflow.${node.type}`)}</span>
|
||||
</Flex>
|
||||
))}
|
||||
</Space>
|
||||
</Flex>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</Flex>
|
||||
</RbCard>
|
||||
<Flex gap={12} vertical>
|
||||
</Flex>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-09 18:31:30
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-09 18:31:30
|
||||
* @Last Modified time: 2026-03-06 11:43:58
|
||||
*/
|
||||
import { useState } from 'react';
|
||||
import { Popover } from 'antd';
|
||||
import { Popover, Flex } from 'antd';
|
||||
import clsx from 'clsx';
|
||||
import type { ReactShapeConfig } from '@antv/x6-react-shape';
|
||||
import { nodeLibrary, graphNodeLibrary, edgeAttrs, nodeWidth } from '../../constant';
|
||||
@@ -20,13 +20,13 @@ const AddNode: ReactShapeConfig['component'] = ({ node, graph }) => {
|
||||
const handleNodeSelect = (selectedNodeType: any) => {
|
||||
const parentBBox = node.getBBox();
|
||||
const cycleId = data.cycle;
|
||||
const horizontalSpacing = 20;
|
||||
const horizontalSpacing = 0;
|
||||
|
||||
const id = `${selectedNodeType.type.replace(/-/g, '_') }_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
||||
const newNode = graph.addNode({
|
||||
...(graphNodeLibrary[selectedNodeType.type] || graphNodeLibrary.default),
|
||||
x: parentBBox.x + horizontalSpacing,
|
||||
y: parentBBox.y,
|
||||
y: parentBBox.y - 12,
|
||||
id,
|
||||
data: {
|
||||
id,
|
||||
@@ -91,11 +91,19 @@ const AddNode: ReactShapeConfig['component'] = ({ node, graph }) => {
|
||||
};
|
||||
}, { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity });
|
||||
|
||||
const padding = 20;
|
||||
const padding = 50;
|
||||
const newWidth = Math.max(nodeWidth, bounds.maxX - bounds.minX + padding * 2);
|
||||
const newHeight = Math.max(120, bounds.maxY - bounds.minY + padding * 2);
|
||||
|
||||
loopNode.prop('size', { width: newWidth, height: newHeight });
|
||||
|
||||
// Update right port x position
|
||||
const ports = loopNode.getPorts();
|
||||
ports.forEach(port => {
|
||||
if (port.group === 'right' && port.args) {
|
||||
loopNode.portProp(port.id!, 'args/x', newWidth);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -161,16 +169,18 @@ const AddNode: ReactShapeConfig['component'] = ({ node, graph }) => {
|
||||
onOpenChange={setOpen}
|
||||
placement="bottomLeft"
|
||||
>
|
||||
<div
|
||||
className={clsx('rb:group rb:relative rb:h-11 rb:w-22 rb:border rb:rounded-xl rb:flex rb:items-center rb:justify-center rb:text-[12px] rb:p-1 rb:box-border rb:cursor-pointer', {
|
||||
'rb:border-orange-500 rb:border-[3px] rb:bg-white rb:text-gray-700': data.isSelected,
|
||||
'rb:border-[#d1d5db] rb:bg-white rb:text-[#374151]': !data.isSelected
|
||||
<Flex
|
||||
align="center"
|
||||
justify="center"
|
||||
gap={4}
|
||||
className={clsx('rb:text-[#212332] rb:font-medium rb:text-[12px] rb:cursor-pointer rb:group rb:relative rb:h-full rb:w-full rb:border rb:rounded-lg rb:bg-[#FCFCFD] rb:shadow-[0px_2px_4px_0px_rgba(23,23,25,0.03)] rb:border-[#DFE4ED] rb:flex rb:items-center rb:justify-center', {
|
||||
'rb:border-orange-500 rb:border-[3px] rb:bg-[#FCFCFD] rb:text-[#475467]': data.isSelected,
|
||||
'rb:border-[#d1d5db] rb:bg-[#FCFCFD] rb:text-[#374151]': !data.isSelected
|
||||
})}
|
||||
>
|
||||
<span className="rb:overflow-hidden rb:whitespace-nowrap rb:text-ellipsis">
|
||||
{data.icon} {data.label}
|
||||
</span>
|
||||
</div>
|
||||
<div className="rb:size-4 rb:bg-cover rb:bg-[url('src/assets/images/workflow/node_plus.png')]"></div>
|
||||
{data.label}
|
||||
</Flex>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,32 +1,65 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import clsx from 'clsx';
|
||||
import type { ReactShapeConfig } from '@antv/x6-react-shape';
|
||||
import { Flex } from 'antd';
|
||||
|
||||
import NodeTools from './NodeTools'
|
||||
|
||||
const caculateIsSet = (item: any, type: string) => {
|
||||
switch(type) {
|
||||
case 'categories':
|
||||
return typeof item?.class_name === 'string' && item?.class_name !== ''
|
||||
case 'cases':
|
||||
return item.expressions.length > 0 && item.expressions.filter((vo: any) => {
|
||||
const keys = Object.keys(vo)
|
||||
return keys.length === 0 || (keys.length > 0
|
||||
&& ((['not_empty', 'empty'].includes(vo.operator) && (['undefined', 'null'].includes(typeof vo.left) || vo.left === ''))
|
||||
|| (!['not_empty', 'empty'].includes(vo.operator) && (['undefined', 'null'].includes(typeof vo.right) || vo.right === ''))))
|
||||
}).length === 0
|
||||
}
|
||||
}
|
||||
const ConditionNode: ReactShapeConfig['component'] = ({ node }) => {
|
||||
const data = node?.getData() || {};
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className={clsx('rb:cursor-pointer rb:group rb:relative rb:h-full rb:w-full rb:p-2.5 rb:border rb:rounded-xl rb:bg-white rb:hover:shadow-[0px_2px_6px_0px_rgba(33,35,50,0.12)]', {
|
||||
'rb:border-[#155EEF]': data.isSelected,
|
||||
<div className={clsx('rb:cursor-pointer rb:group rb:relative rb:h-full rb:w-full rb:p-3 rb:border rb:rounded-2xl rb:bg-[#FCFCFD] rb:shadow-[0px_2px_4px_0px_rgba(23,23,25,0.03)]', {
|
||||
'rb:border-[#171719]': data.isSelected,
|
||||
'rb:border-[#DFE4ED]': !data.isSelected
|
||||
})}>
|
||||
<div className="rb:flex rb:items-center rb:justify-between">
|
||||
<div className="rb:flex rb:items-center rb:gap-2 rb:flex-1">
|
||||
<img src={data.icon} className="rb:w-5 rb:h-5" />
|
||||
<NodeTools node={node} />
|
||||
<Flex align="center" gap={8} className="rb:flex-1">
|
||||
<img src={data.icon} className="rb:size-6" />
|
||||
<div className="rb:wrap-break-word rb:line-clamp-1">{data.name ?? t(`workflow.${data.type}`)}</div>
|
||||
</div>
|
||||
</Flex>
|
||||
|
||||
<div
|
||||
className="rb:w-5 rb:h-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/deleteBorder.svg')] rb:hover:bg-[url('@/assets/images/deleteBg.svg')]"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
node.remove()
|
||||
}}
|
||||
></div>
|
||||
{data.type === 'question-classifier' &&
|
||||
<Flex vertical gap={4} className="rb:mt-3!">
|
||||
{data.config?.categories?.defaultValue.map((item: any, index: number) => (
|
||||
<div key={index} className="rb:bg-[#F0F3F8] rb:shadow-[0px_2px_4px_0px_rgba(23,23,25,0.03)] rb:rounded-md rb:py-1 rb:px-1.5 rb:text-[10px] rb:text-[#5B6167] rb:font-medium rb:leading-3.5">
|
||||
<Flex justify="space-between">
|
||||
<span>{t('workflow.config.question-classifier.class_name')} {index + 1}</span>
|
||||
{caculateIsSet(item, 'categories') ? t(`workflow.config.${data.type}.set`) : t(`workflow.config.${data.type}.unset`)}
|
||||
</Flex>
|
||||
</div>
|
||||
|
||||
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4 rb:mt-7">{t('workflow.clickToConfigure')}</div>
|
||||
))}
|
||||
</Flex>
|
||||
}
|
||||
{data.type === 'if-else' &&
|
||||
<Flex vertical gap={4} className="rb:mt-3!">
|
||||
{data.config?.cases?.defaultValue.map((item: any, index: number) => (
|
||||
<div key={index} className="rb:bg-[#F0F3F8] rb:shadow-[0px_2px_4px_0px_rgba(23,23,25,0.03)] rb:rounded-md rb:py-1 rb:px-1.5 rb:text-[10px] rb:text-[#5B6167] rb:font-medium rb:leading-3.5">
|
||||
<Flex justify="space-between">
|
||||
<span>{index === 0 ? 'IF' : `ELIF`}</span>
|
||||
{caculateIsSet(item, 'cases') ? t(`workflow.config.${data.type}.set`) : t(`workflow.config.${data.type}.unset`)}
|
||||
</Flex>
|
||||
</div>
|
||||
))}
|
||||
<div className="rb:bg-[#F0F3F8] rb:shadow-[0px_2px_4px_0px_rgba(23,23,25,0.03)] rb:rounded-md rb:py-1 rb:px-1.5 rb:text-[10px] rb:text-[#5B6167] rb:font-medium rb:leading-3.5">
|
||||
ELSE
|
||||
</div>
|
||||
</Flex>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import clsx from 'clsx';
|
||||
import type { ReactShapeConfig } from '@antv/x6-react-shape';
|
||||
import startIcon from '@/assets/images/workflow/start.png';
|
||||
|
||||
const GroupStartNode: ReactShapeConfig['component'] = () => {
|
||||
return (
|
||||
<div className={clsx('rb:cursor-pointer rb:group rb:relative rb:h-full rb:w-full rb:p-2.5 rb:border rb:rounded-xl rb:bg-white rb:hover:shadow-[0px_2px_6px_0px_rgba(33,35,50,0.12)] rb:border-[#DFE4ED]')}>
|
||||
<img src={startIcon} className="rb:w-6 rb:h-6" />
|
||||
<div className={clsx('rb:cursor-pointer rb:group rb:relative rb:h-full rb:w-full rb:border rb:rounded-xl rb:bg-[#FCFCFD] rb:shadow-[0px_2px_4px_0px_rgba(23,23,25,0.03)] rb:border-[#DFE4ED] rb:flex rb:items-center rb:justify-center')}>
|
||||
<div className="rb:size-5 rb:bg-cover rb:bg-[url('@/assets/images/workflow/start.svg')]" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,7 +2,10 @@ import { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import clsx from 'clsx';
|
||||
import type { ReactShapeConfig } from '@antv/x6-react-shape';
|
||||
import { Flex } from 'antd';
|
||||
|
||||
import { graphNodeLibrary, edgeAttrs } from '../../constant';
|
||||
import NodeTools from './NodeTools'
|
||||
|
||||
const LoopNode: ReactShapeConfig['component'] = ({ node, graph }) => {
|
||||
const data = node.getData() || {};
|
||||
@@ -32,7 +35,7 @@ const LoopNode: ReactShapeConfig['component'] = ({ node, graph }) => {
|
||||
const addNode = graph.addNode({
|
||||
...graphNodeLibrary.addStart,
|
||||
x: cycleStartBBox.x + 84,
|
||||
y: cycleStartBBox.y,
|
||||
y: cycleStartBBox.y + 4,
|
||||
data: {
|
||||
type: 'add-node',
|
||||
label: t('workflow.addNode'),
|
||||
@@ -67,8 +70,8 @@ const LoopNode: ReactShapeConfig['component'] = ({ node, graph }) => {
|
||||
if (existingCycleNodes.length > 0) return;
|
||||
// 添加默认子节点
|
||||
const parentBBox = node.getBBox();
|
||||
const centerX = parentBBox.x + 24; // 默认节点宽度的一半
|
||||
const centerY = parentBBox.y + 50; // 默认节点高度的一半
|
||||
const centerX = parentBBox.x + 24;
|
||||
const centerY = parentBBox.y + 70;
|
||||
|
||||
const cycleStartNodeId = `cycle_start_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
||||
const cycleStartNode = graph.addNode({
|
||||
@@ -87,7 +90,7 @@ const LoopNode: ReactShapeConfig['component'] = ({ node, graph }) => {
|
||||
const addNode = graph.addNode({
|
||||
...graphNodeLibrary.addStart,
|
||||
x: centerX + 84,
|
||||
y: centerY,
|
||||
y: centerY + 4,
|
||||
data: {
|
||||
type: 'add-node',
|
||||
label: t('workflow.addNode'),
|
||||
@@ -117,25 +120,16 @@ const LoopNode: ReactShapeConfig['component'] = ({ node, graph }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={clsx('rb:cursor-pointer rb:group rb:relative rb:h-full rb:w-full rb:p-2.5 rb:border rb:rounded-xl rb:bg-white rb:hover:shadow-[0px_2px_6px_0px_rgba(33,35,50,0.12)]', {
|
||||
'rb:border-[#155EEF]': data.isSelected,
|
||||
<div className={clsx('rb:cursor-pointer rb:group rb:relative rb:h-full rb:w-full rb:p-3 rb:border rb:rounded-2xl rb:bg-[#FCFCFD] rb:shadow-[0px_2px_4px_0px_rgba(23,23,25,0.03)]', {
|
||||
'rb:border-[#171719]': data.isSelected,
|
||||
'rb:border-[#DFE4ED]': !data.isSelected
|
||||
})}>
|
||||
<div className="rb:flex rb:items-center rb:justify-between">
|
||||
<div className="rb:flex rb:items-center rb:gap-2 rb:flex-1">
|
||||
<img src={data.icon} className="rb:w-5 rb:h-5" />
|
||||
<NodeTools node={node} />
|
||||
<Flex align="center" gap={8} className="rb:flex-1">
|
||||
<img src={data.icon} className="rb:size-6" />
|
||||
<div className="rb:wrap-break-word rb:line-clamp-1">{data.name ?? t(`workflow.${data.type}`)}</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="rb:w-5 rb:h-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/deleteBorder.svg')] rb:hover:bg-[url('@/assets/images/deleteBg.svg')]"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
node.remove()
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
<div className="rb:mt-3 rb:min-h-[calc(100%-36px)] rb:w-full rb:bg-[radial-gradient(circle,#e5e7eb_1px,transparent_1px)] rb:bg-size-[12px_12px]"></div>
|
||||
</Flex>
|
||||
<div className="rb:mt-3 rb:min-h-[calc(100%-36px)] rb:w-full rb:bg-[radial-gradient(circle,#939AB1_1px,#F0F3F8_1px)] rb:shadow-[0px_2px_4px_0px_rgba(23,23,25,0.03)] rb:rounded-[10px] rb:bg-size-[12px_12px]"></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
43
web/src/views/Workflow/components/Nodes/NodeTools.tsx
Normal file
43
web/src/views/Workflow/components/Nodes/NodeTools.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { type FC } from 'react';
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import clsx from 'clsx';
|
||||
import { Node } from '@antv/x6';
|
||||
import { Flex, Dropdown, type MenuProps } from 'antd';
|
||||
|
||||
const NodeTools: FC<{ node: Node }> = ({
|
||||
node
|
||||
}) => {
|
||||
const data = node?.getData() || {};
|
||||
const { t } = useTranslation()
|
||||
|
||||
const handleClick: MenuProps['onClick'] = (e) => {
|
||||
switch (e.key) {
|
||||
case 'delete':
|
||||
node.remove()
|
||||
break;
|
||||
case 'copy':
|
||||
break;
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div className={clsx("rb:absolute rb:p-1 rb:bg-white rb:-top-7.5 rb:right-0 rb:rounded-lg", {
|
||||
'rb:block': data.isSelected,
|
||||
'rb:hidden': !data.isSelected
|
||||
})}>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{ key: 'delete', icon: <div className="rb:size-4 rb:bg-cover rb:bg-[url('src/assets/images/common/delete_dark.svg')]"></div>, label: <Flex>{t('common.delete')}</Flex>},
|
||||
// { key: 'copy', icon: <div className="rb:size-4 rb:bg-cover rb:bg-[url('@/assets/images/common/copy_dark.svg')]"></div>, label: t('common.copy') }
|
||||
],
|
||||
onClick: handleClick
|
||||
}}
|
||||
>
|
||||
<div className="rb:cursor-pointer rb:size-4 rb:hover:bg-[#F6F6F6] rb:rounded-sm rb:bg-cover rb:bg-[url(@/assets/images/common/dash.svg)]">
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default NodeTools;
|
||||
@@ -1,32 +1,26 @@
|
||||
import clsx from 'clsx';
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { ReactShapeConfig } from '@antv/x6-react-shape';
|
||||
import { Flex } from 'antd';
|
||||
|
||||
import NodeTools from './NodeTools'
|
||||
|
||||
const NormalNode: ReactShapeConfig['component'] = ({ node }) => {
|
||||
const data = node?.getData() || {}
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className={clsx('rb:cursor-pointer rb:group rb:relative rb:h-full rb:w-full rb:p-2.5 rb:border rb:rounded-xl rb:bg-white rb:hover:shadow-[0px_2px_6px_0px_rgba(33,35,50,0.12)]', {
|
||||
'rb:border-[#155EEF]': data.isSelected,
|
||||
<div className={clsx('rb:cursor-pointer rb:group rb:relative rb:h-full rb:w-full rb:p-3 rb:border rb:rounded-2xl rb:bg-[#FCFCFD] rb:shadow-[0px_2px_4px_0px_rgba(23,23,25,0.03)]', {
|
||||
'rb:border-[#171719]': data.isSelected,
|
||||
'rb:border-[#DFE4ED]': !data.isSelected
|
||||
})}>
|
||||
<div className="rb:flex rb:items-center rb:justify-between">
|
||||
<div className="rb:flex rb:items-center rb:gap-2 rb:flex-1">
|
||||
<img src={data.icon} className="rb:w-5 rb:h-5" />
|
||||
<NodeTools node={node} />
|
||||
<Flex align="center" gap={8} className="rb:flex-1">
|
||||
<img src={data.icon} className="rb:size-6" />
|
||||
<div className="rb:wrap-break-word rb:line-clamp-1">{data.name ?? t(`workflow.${data.type}`)}</div>
|
||||
</div>
|
||||
</Flex>
|
||||
|
||||
<div
|
||||
className="rb:w-5 rb:h-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/deleteBorder.svg')] rb:hover:bg-[url('@/assets/images/deleteBg.svg')]"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
node.remove()
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4 rb:mt-1.5">{t('workflow.clickToConfigure')}</div>
|
||||
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4 rb:mt-3">{t('workflow.clickToConfigure')}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-09 18:30:28
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-09 18:30:28
|
||||
* @Last Modified time: 2026-03-06 11:49:30
|
||||
*/
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Popover } from 'antd';
|
||||
@@ -47,7 +47,8 @@ const PortClickHandler: React.FC<PortClickHandlerProps> = ({ graph }) => {
|
||||
|
||||
// If it's a cycle-start node, handle the add-node placeholder
|
||||
let addNodePosition = null;
|
||||
if (sourceNodeType === 'cycle-start' && sourceNodeData.cycle) {
|
||||
const isCycleSubNode = sourceNodeData.cycle
|
||||
if (isCycleSubNode && sourceNodeType === 'cycle-start') {
|
||||
const cycleId = sourceNodeData.cycle;
|
||||
const addNodes = graph.getNodes().filter((n: any) =>
|
||||
n.getData()?.type === 'add-node' && n.getData()?.cycle === cycleId
|
||||
@@ -64,13 +65,12 @@ const PortClickHandler: React.FC<PortClickHandlerProps> = ({ graph }) => {
|
||||
const sourceBBox = sourceNode.getBBox();
|
||||
const nodeWidth = graphNodeLibrary[selectedNodeType.type]?.width || 120;
|
||||
const nodeHeight = graphNodeLibrary[selectedNodeType.type]?.height || 88;
|
||||
const horizontalSpacing = sourceNodeType === 'cycle-start' ? 40 : 80;
|
||||
const horizontalSpacing = isCycleSubNode ? 48 : 80;
|
||||
const verticalSpacing = 10;
|
||||
|
||||
// Get source port group information
|
||||
const sourcePortInfo = sourceNode.getPorts().find((p: any) => p.id === sourcePort);
|
||||
const sourcePortGroup = sourcePortInfo?.group || sourcePort;
|
||||
console.log('sourcePortGroup', sourcePortGroup, sourcePortInfo)
|
||||
|
||||
// If add-node position exists, use it; otherwise calculate new position
|
||||
let newX, newY;
|
||||
@@ -120,7 +120,7 @@ const PortClickHandler: React.FC<PortClickHandlerProps> = ({ graph }) => {
|
||||
const newNode = graph.addNode({
|
||||
...(graphNodeLibrary[selectedNodeType.type] || graphNodeLibrary.default),
|
||||
x: newX,
|
||||
y: newY,
|
||||
y: newY - (isCycleSubNode && sourceNodeType === 'cycle-start' ? 12 : 0),
|
||||
id,
|
||||
data: {
|
||||
id,
|
||||
@@ -179,12 +179,19 @@ const PortClickHandler: React.FC<PortClickHandlerProps> = ({ graph }) => {
|
||||
};
|
||||
}, { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity });
|
||||
|
||||
const padding = 20;
|
||||
const bottomPadding = 50;
|
||||
const padding = 50;
|
||||
const newWidth = Math.max(nodeWidth, bounds.maxX - bounds.minX + padding * 2);
|
||||
const newHeight = Math.max(120, bounds.maxY - bounds.minY + padding + bottomPadding);
|
||||
const newHeight = Math.max(120, bounds.maxY - bounds.minY + padding * 2);
|
||||
|
||||
parentNode.prop('size', { width: newWidth, height: newHeight });
|
||||
|
||||
// Update right port x position
|
||||
const ports = parentNode.getPorts();
|
||||
ports.forEach((port: any) => {
|
||||
if (port.group === 'right' && port.args) {
|
||||
parentNode.portProp(port.id!, 'args/x', newWidth);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { type FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Form, Input, Select, InputNumber, Radio, Button, Space } from 'antd'
|
||||
import { Form, Input, Select, InputNumber, Button, Flex } from 'antd'
|
||||
|
||||
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
||||
import VariableSelect from '../VariableSelect'
|
||||
import RadioGroupBtn from '../RadioGroupBtn'
|
||||
|
||||
interface AssignmentListProps {
|
||||
value?: Array<{ variable_selector: string; operation: string[]; value: string;}>;
|
||||
@@ -40,21 +42,21 @@ const AssignmentList: FC<AssignmentListProps> = ({
|
||||
<Form.List name={parentName}>
|
||||
{(fields, { add, remove }) => (
|
||||
<>
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mb-2.5">
|
||||
<Flex align="center" justify="space-between" className="rb:mb-2.5!">
|
||||
<div className="rb:text-[12px] rb:leading-4.5 rb:font-medium">
|
||||
{t(`workflow.config.assigner.${parentName}`)}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={() => add({ operation: 'cover' })}
|
||||
className="rb:py-0! rb:px-1! rb:text-[12px]!"
|
||||
className="rb:py-0! rb:px-1! rb:h-4.5! rb:rounded-sm! rb:text-[12px]!"
|
||||
size="small"
|
||||
>
|
||||
+ {t('workflow.config.addVariable')}
|
||||
</Button>
|
||||
</div>
|
||||
</Flex >
|
||||
|
||||
<Space size={10} direction="vertical" className="rb:w-full!">
|
||||
<Flex gap={10} vertical>
|
||||
{fields.map(({ key, name, ...restField }) => {
|
||||
const variableSelector = form.getFieldValue([parentName, name, 'variable_selector']);
|
||||
const selectedOption = options.find(option => `{{${option.value}}}` === variableSelector);
|
||||
@@ -62,9 +64,9 @@ const AssignmentList: FC<AssignmentListProps> = ({
|
||||
const operationOptions = dataType === 'number' ? operationsObj.number : operationsObj.default;
|
||||
|
||||
return (
|
||||
<div key={key} className="rb:flex rb:items-start">
|
||||
<Flex key={key} gap={4} align="start">
|
||||
<div className="rb:flex-1">
|
||||
<div className="rb:flex rb:gap-1 rb:mb-1">
|
||||
<Flex gap={4} className="rb:mb-1!">
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 'variable_selector']}
|
||||
@@ -79,7 +81,8 @@ const AssignmentList: FC<AssignmentListProps> = ({
|
||||
form.setFieldValue([parentName, name, 'value'], undefined);
|
||||
}}
|
||||
size={size}
|
||||
className="rb:w-39!"
|
||||
className="rb:w-39! rb:bg-[#F6F6F6]!"
|
||||
variant="borderless"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
@@ -98,10 +101,11 @@ const AssignmentList: FC<AssignmentListProps> = ({
|
||||
form.setFieldValue([parentName, name, 'value'], undefined);
|
||||
}}
|
||||
size={size}
|
||||
className="rb:w-24!"
|
||||
className="rb:w-39! select"
|
||||
variant="borderless"
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
</Flex>
|
||||
<Form.Item shouldUpdate noStyle>
|
||||
{(form) => {
|
||||
const operation = form.getFieldValue([parentName, name, 'operation']);
|
||||
@@ -119,24 +123,30 @@ const AssignmentList: FC<AssignmentListProps> = ({
|
||||
options={dataType ? options.filter(vo => vo.dataType === dataType) : options}
|
||||
popupMatchSelectWidth={false}
|
||||
size={size}
|
||||
variant="borderless"
|
||||
className="select"
|
||||
/>
|
||||
: dataType === 'number'
|
||||
? <InputNumber
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
className="rb:w-full!"
|
||||
className="rb:w-full! rb:bg-[#F6F6F6]!"
|
||||
onChange={(value) => form.setFieldValue([name, 'value'], value)}
|
||||
size={size}
|
||||
variant="borderless"
|
||||
/>
|
||||
: operation === 'assign'
|
||||
? <>
|
||||
{dataType === 'boolean'
|
||||
? <Radio.Group block size={size}>
|
||||
<Radio.Button value={true}>True</Radio.Button>
|
||||
<Radio.Button value={false}>False</Radio.Button>
|
||||
</Radio.Group>
|
||||
? <RadioGroupBtn
|
||||
options={[
|
||||
{ value: true, label: 'True' },
|
||||
{ value: false, label: 'False' }]}
|
||||
/>
|
||||
: <Input.TextArea
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
rows={3}
|
||||
variant="borderless"
|
||||
className="rb:bg-[#F6F6F6]!"
|
||||
/>
|
||||
}
|
||||
</>
|
||||
@@ -145,6 +155,8 @@ const AssignmentList: FC<AssignmentListProps> = ({
|
||||
options={dataType ? options.filter(vo => vo.dataType === dataType) : options}
|
||||
popupMatchSelectWidth={false}
|
||||
size={size}
|
||||
variant="borderless"
|
||||
className="select"
|
||||
/>
|
||||
}
|
||||
</Form.Item>
|
||||
@@ -153,13 +165,13 @@ const AssignmentList: FC<AssignmentListProps> = ({
|
||||
</Form.Item>
|
||||
</div>
|
||||
<div
|
||||
className="rb:ml-1 rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/workflow/deleteBg.svg')] rb:hover:bg-[url('@/assets/images/workflow/deleteBg_hover.svg')]"
|
||||
className="rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/workflow/deleteBg.svg')] rb:hover:bg-[url('@/assets/images/workflow/deleteBg_hover.svg')]"
|
||||
onClick={() => remove(name)}
|
||||
></div>
|
||||
</div>
|
||||
</Flex>
|
||||
)
|
||||
})}
|
||||
</Space>
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
|
||||
@@ -2,17 +2,19 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-09 18:24:53
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-28 17:49:28
|
||||
* @Last Modified time: 2026-03-06 14:24:57
|
||||
*/
|
||||
import { type FC } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Form, Button, Select, Space, Divider, InputNumber, Radio, type SelectProps } from 'antd'
|
||||
import { Form, Button, Select, Space, Divider, InputNumber, type SelectProps, Flex, Row, Col } from 'antd'
|
||||
|
||||
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
||||
import VariableSelect from '../VariableSelect'
|
||||
import Editor from '../../Editor'
|
||||
import { edgeAttrs, portTextAttrs, nodeWidth } from '../../../constant'
|
||||
import { edgeAttrs, conditionNodeItemHeight, nodeWidth, portItemArgsY, conditionNodePortItemArgsY, conditionNodeHeight } from '../../../constant'
|
||||
import RbButton from '@/components/RbButton';
|
||||
import RadioGroupBtn from '../RadioGroupBtn'
|
||||
|
||||
interface CaseListProps {
|
||||
value?: Array<{ logical_operator: 'and' | 'or'; expressions: { left: string; operator: string; right: string; input_type?: string; }[] }>;
|
||||
@@ -89,33 +91,26 @@ const CaseList: FC<CaseListProps> = ({
|
||||
});
|
||||
|
||||
// Calculate new node height: base height 88px + 30px for each additional port
|
||||
const baseHeight = 88;
|
||||
const totalPorts = caseCount + 1; // IF/ELIF + ELSE
|
||||
const newHeight = baseHeight + (totalPorts - 2) * 30;
|
||||
const newHeight = conditionNodeHeight + (totalPorts - 2) * conditionNodeItemHeight;
|
||||
|
||||
selectedNode.prop('size', { width: nodeWidth, height: newHeight })
|
||||
|
||||
// Add IF port
|
||||
selectedNode.addPort({
|
||||
id: 'CASE1',
|
||||
group: 'right',
|
||||
args: {
|
||||
x: nodeWidth,
|
||||
y: 42,
|
||||
},
|
||||
attrs: { text: { text: 'IF', ...portTextAttrs } }
|
||||
})
|
||||
|
||||
// Update right port x position
|
||||
currentPorts.forEach((port: any) => {
|
||||
if (port.group === 'right' && port.args) {
|
||||
selectedNode.portProp(port.id!, 'args/x', nodeWidth);
|
||||
}
|
||||
});
|
||||
// Add ELIF ports
|
||||
for (let i = 1; i < caseCount; i++) {
|
||||
for (let i = 0; i < caseCount; i++) {
|
||||
selectedNode.addPort({
|
||||
id: `CASE${i + 1}`,
|
||||
group: 'right',
|
||||
args: {
|
||||
x: nodeWidth,
|
||||
y: 30 * i + 42,
|
||||
y: portItemArgsY * i + conditionNodePortItemArgsY,
|
||||
},
|
||||
attrs: { text: { text: 'ELIF', ...portTextAttrs }}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -125,9 +120,8 @@ const CaseList: FC<CaseListProps> = ({
|
||||
group: 'right',
|
||||
args: {
|
||||
x: nodeWidth,
|
||||
y: 30 * caseCount + 42,
|
||||
y: portItemArgsY * caseCount + conditionNodePortItemArgsY,
|
||||
},
|
||||
attrs: { text: { text: 'ELSE', ...portTextAttrs }}
|
||||
});
|
||||
|
||||
// Restore edge connections
|
||||
@@ -201,19 +195,11 @@ const CaseList: FC<CaseListProps> = ({
|
||||
};
|
||||
|
||||
const handleLeftFieldChange = (caseIndex: number, conditionIndex: number, newValue: string) => {
|
||||
form.setFieldsValue({
|
||||
[name]: {
|
||||
[caseIndex]: {
|
||||
expressions: {
|
||||
[conditionIndex]: {
|
||||
form.setFieldValue([name, caseIndex, 'expressions', conditionIndex], {
|
||||
left: newValue,
|
||||
operator: undefined,
|
||||
right: undefined,
|
||||
input_type: undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -248,40 +234,27 @@ const CaseList: FC<CaseListProps> = ({
|
||||
{(conditionFields, { add: addCondition, remove: removeCondition }) => {
|
||||
const logicalOperator = form.getFieldValue(name)?.[caseIndex]?.logical_operator || 'and'
|
||||
return (
|
||||
<div className={clsx("rb:relative")}>
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mb-2">
|
||||
<div className="rb:text-[12px] rb:leading-4.5">
|
||||
<span className="rb:font-medium ">{caseIndex === 0 ? 'IF' : 'ELIF'}</span>
|
||||
{caseFields.length > 1 && <span className="rb:text-[10px] rb:text-[#5B6167]"> ({`CASE ${caseIndex + 1}`})</span>}
|
||||
</div>
|
||||
|
||||
<Space>
|
||||
<Button
|
||||
onClick={() => addCondition({})}
|
||||
className="rb:py-0! rb:px-1! rb:text-[12px]!"
|
||||
size="small"
|
||||
>
|
||||
+ {t('workflow.config.addCase')}
|
||||
</Button>
|
||||
{caseFields.length > 1 &&
|
||||
<Button
|
||||
className="rb:py-0! rb:px-1! rb:text-[12px]!"
|
||||
onClick={() => handleRemoveCase(removeCase, caseField.name, caseIndex)}
|
||||
>
|
||||
{t('common.remove')}
|
||||
</Button>
|
||||
}
|
||||
</Space>
|
||||
</div>
|
||||
{conditionFields?.length > 1 && <div className="rb:absolute rb:top-8 rb:bottom-4 rb:w-8.5 rb:h-[calc(100%-32px)]">
|
||||
<div className="rb:absolute rb:w-2.5 rb:h-[calc(50%-30px)] rb:left-5 rb:top-4 rb:z-10 rb:border-l rb:border-t rb:border-[#DFE4ED] rb:rounded-tl-[10px] rb:border-r-0"></div>
|
||||
<div className="rb:absolute rb:z-10 rb:left-0 rb:top-[calc(50%-13px)]">
|
||||
<Row className="rb:text-[12px] rb:mb-4!">
|
||||
<Col flex="48px">
|
||||
<div className="rb:font-medium rb:leading-4.5">{caseIndex === 0 ? 'IF' : 'ELIF'}</div>
|
||||
{caseFields.length > 1 && <div className="rb:text-[10px] rb:text-[#5B6167] rb:leading-2.5"> {`CASE ${caseIndex + 1}`}</div>}
|
||||
</Col>
|
||||
<Col flex="1" className="rb:pl-3!">
|
||||
<div className="rb:relative">
|
||||
{conditionFields?.length > 1 && (
|
||||
<div className="rb:absolute rb:-left-9 rb:top-4 rb:bottom-4 rb:w-6 rb:h-[calc(100%-32px)]">
|
||||
<div className="rb:absolute rb:w-3 rb:h-[calc(50%-20px)] rb:left-5 rb:top-0 rb:z-10 rb:border-l rb:border-t rb:border-[#EBEBEB] rb:rounded-tl-[10px] rb:border-r-0"></div>
|
||||
<div className="rb:absolute rb:z-10 rb:-right-1.25 rb:top-[calc(50%-10px)]">
|
||||
<Form.Item name={[caseField.name, 'logical_operator']} noStyle >
|
||||
<Button size="small" className="rb:text-[12px]! rb:py-px! rb:px-1! rb:w-8.5! rb:h-5!" onClick={() => handleChangeLogicalOperator(caseIndex)}>{logicalOperator}</Button>
|
||||
<Space size={2} className="rb:cursor-pointer rb:text-[#155EEF] rb:leading-4.5 rb:font-medium rb-border rb:py-px! rb:px-1! rb:rounded-sm" onClick={() => handleChangeLogicalOperator(caseIndex)}>
|
||||
{logicalOperator}
|
||||
<div className="rb:size-3 rb:bg-cover rb:bg-[url('src/assets/images/workflow/refresh_active.svg')]"></div>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</div>
|
||||
<div className="rb:absolute rb:w-2.5 rb:h-[calc(50%-30px)] rb:left-5 rb:bottom-4 rb:z-10 rb:border-l rb:border-b rb:border-[#DFE4ED] rb:rounded-bl-[10px] rb:border-r-0"></div>
|
||||
</div>}
|
||||
<div className="rb:absolute rb:w-3 rb:h-[calc(50%-20px)] rb:left-5 rb:bottom-0 rb:z-10 rb:border-l rb:border-b rb:border-[#EBEBEB] rb:rounded-bl-[10px] rb:border-r-0"></div>
|
||||
</div>
|
||||
)}
|
||||
{conditionFields.map((conditionField, conditionIndex) => {
|
||||
const cases = form.getFieldValue(name) || [];
|
||||
const currentCase = cases[caseIndex] || {};
|
||||
@@ -292,13 +265,14 @@ const CaseList: FC<CaseListProps> = ({
|
||||
const leftFieldOption = options.find(option => `{{${option.value}}}` === leftFieldValue);
|
||||
const leftFieldType = leftFieldOption?.dataType;
|
||||
const operatorList = operatorsObj[leftFieldType || 'default'] || operatorsObj.default || [];
|
||||
const inputType = leftFieldType === 'number' ? currentExpression.input_type?.toLocaleLowerCase() : undefined;
|
||||
const inputType = leftFieldType === 'number' ? currentExpression.input_type : undefined;
|
||||
return (
|
||||
<div key={conditionField.key} className="rb:flex rb:items-start rb:ml-9.5 rb:mb-4">
|
||||
<div className="rb:flex-1 rb:bg-[#F6F8FC] rb:border rb:border-[#DFE4ED] rb:rounded-md">
|
||||
<div className={clsx("rb:flex rb:gap-1 rb:p-1", {
|
||||
'rb:border-b rb:border-b-[#DFE4ED]': !hideRightField
|
||||
<Flex key={conditionField.key} gap={4} align="start" className="rb:mb-2!">
|
||||
<div className="rb:flex-1 rb:bg-[#F6F6F6] rb:rounded-lg">
|
||||
<Row className={clsx("rb:p-1!", {
|
||||
'rb-border-b': !hideRightField
|
||||
})}>
|
||||
<Col flex="144px">
|
||||
<Form.Item name={[conditionField.name, 'left']} noStyle>
|
||||
<VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
@@ -307,9 +281,12 @@ const CaseList: FC<CaseListProps> = ({
|
||||
allowClear={false}
|
||||
popupMatchSelectWidth={false}
|
||||
onChange={(val) => handleLeftFieldChange(caseIndex, conditionIndex, val)}
|
||||
className="rb:bg-white! rb:w-29.5!"
|
||||
variant="borderless"
|
||||
className="rb:w-36!"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col flex="1">
|
||||
<Form.Item name={[conditionField.name, 'operator']} noStyle>
|
||||
<Select
|
||||
options={operatorList.map(vo => ({
|
||||
@@ -319,29 +296,31 @@ const CaseList: FC<CaseListProps> = ({
|
||||
size="small"
|
||||
popupMatchSelectWidth={false}
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
className="rb:bg-white! rb:w-22!"
|
||||
variant="borderless"
|
||||
className="rb:w-full!"
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{!hideRightField && <div className="rb:p-1">
|
||||
{!hideRightField && (
|
||||
<div className="rb:py-1 rb:px-1.5">
|
||||
{leftFieldType === 'number'
|
||||
? <div className="rb:flex rb:items-center">
|
||||
? <Flex align="center">
|
||||
<Form.Item name={[conditionField.name, 'input_type']} noStyle>
|
||||
<Select
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={[{ value: 'variable', label: 'Variable' }, { value: 'constant', label: 'Constant' }]}
|
||||
options={[{ value: 'variable', label: 'Variable' }, { value: 'Constant', label: 'constant' }]}
|
||||
popupMatchSelectWidth={false}
|
||||
variant="borderless"
|
||||
onChange={() => handleInputTypeChange(caseIndex, conditionIndex)}
|
||||
className="rb:w-18!"
|
||||
className="rb:w-20!"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Divider type="vertical" />
|
||||
<Form.Item name={[conditionField.name, 'right']} noStyle>
|
||||
{inputType === 'variable'
|
||||
?
|
||||
<VariableSelect
|
||||
{inputType === 'Variable'
|
||||
? <VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={options.filter(vo => vo.dataType === 'number')}
|
||||
allowClear={false}
|
||||
@@ -357,27 +336,50 @@ const CaseList: FC<CaseListProps> = ({
|
||||
/>
|
||||
}
|
||||
</Form.Item>
|
||||
</div>
|
||||
: <Form.Item name={[conditionField.name, 'right']} noStyle>
|
||||
</Flex>
|
||||
: (
|
||||
<Form.Item name={[conditionField.name, 'right']} noStyle>
|
||||
{leftFieldType === 'boolean'
|
||||
? <Radio.Group block>
|
||||
<Radio.Button value={true}>True</Radio.Button>
|
||||
<Radio.Button value={false}>False</Radio.Button>
|
||||
</Radio.Group>
|
||||
? <RadioGroupBtn options={[ { value: true, label: 'True' }, { value: false, label: 'False' }]} type="inner" />
|
||||
: <Editor options={options} size="small" type="input" />
|
||||
}
|
||||
</Form.Item>
|
||||
)
|
||||
}
|
||||
</div>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="rb:ml-1 rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/workflow/deleteBg.svg')] rb:hover:bg-[url('@/assets/images/workflow/deleteBg_hover.svg')]"
|
||||
className="rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/workflow/deleteBg.svg')] rb:hover:bg-[url('@/assets/images/workflow/deleteBg_hover.svg')]"
|
||||
onClick={() => removeCondition(conditionField.name)}
|
||||
></div>
|
||||
</div>
|
||||
</Flex>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<Row>
|
||||
<Col flex="1">
|
||||
<Button
|
||||
onClick={() => addCondition({})}
|
||||
className={clsx("rb:py-0! rb:px-1! rb:h-4.5! rb:rounded-sm! rb:text-[12px]!")}
|
||||
size="small"
|
||||
>
|
||||
+ {t('workflow.config.addCase')}
|
||||
</Button>
|
||||
</Col>
|
||||
<Col flex="70px">
|
||||
<RbButton
|
||||
danger
|
||||
className="rb:group rb:mr-5 rb:py-0! rb:px-1! rb:h-4.5! rb:rounded-sm! rb:text-[12px]! rb:gap-0!"
|
||||
icon={<div className="rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/workflow/delete.svg')] rb:group-hover:bg-[url('@/assets/images/workflow/delete_hover.svg')]"></div>}
|
||||
onClick={() => handleRemoveCase(removeCase, caseField.name, caseIndex)}
|
||||
>
|
||||
{t('common.remove')}
|
||||
</RbButton>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
}}
|
||||
</Form.List>
|
||||
|
||||
@@ -2,16 +2,16 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-09 18:34:33
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-09 18:34:33
|
||||
* @Last Modified time: 2026-03-05 18:18:35
|
||||
*/
|
||||
import { type FC } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, Form, Space } from 'antd';
|
||||
import { Button, Form, Flex } from 'antd';
|
||||
import { Graph, Node } from '@antv/x6';
|
||||
|
||||
import Editor from '../../Editor';
|
||||
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
||||
import { edgeAttrs, portTextAttrs, nodeWidth } from '../../../constant'
|
||||
import { edgeAttrs, conditionNodeItemHeight, nodeWidth, portItemArgsY, conditionNodePortItemArgsY, conditionNodeHeight } from '../../../constant'
|
||||
|
||||
interface CategoryListProps {
|
||||
parentName: string;
|
||||
@@ -51,10 +51,17 @@ const CategoryList: FC<CategoryListProps> = ({ parentName, selectedNode, graphRe
|
||||
});
|
||||
|
||||
// Calculate new node height: base height 88px + 30px for each additional port
|
||||
const baseHeight = 88;
|
||||
const newHeight = baseHeight + (caseCount - 2) * 30;
|
||||
const newHeight = conditionNodeHeight + (caseCount - 2) * conditionNodeItemHeight;
|
||||
|
||||
selectedNode.prop('size', { width: nodeWidth, height: newHeight < baseHeight ? baseHeight : newHeight })
|
||||
selectedNode.prop('size', { width: nodeWidth, height: newHeight < conditionNodeHeight ? conditionNodeHeight : newHeight })
|
||||
|
||||
// Update right port x position
|
||||
const currentPorts = selectedNode.getPorts();
|
||||
currentPorts.forEach(port => {
|
||||
if (port.group === 'right' && port.args) {
|
||||
selectedNode.portProp(port.id!, 'args/x', nodeWidth);
|
||||
}
|
||||
});
|
||||
|
||||
// Add category ports
|
||||
for (let i = 0; i < caseCount; i++) {
|
||||
@@ -63,9 +70,8 @@ const CategoryList: FC<CategoryListProps> = ({ parentName, selectedNode, graphRe
|
||||
group: 'right',
|
||||
args: {
|
||||
x: nodeWidth,
|
||||
y: 30 * i + 42,
|
||||
y: portItemArgsY * i + conditionNodePortItemArgsY,
|
||||
},
|
||||
attrs: { text: { text: `分类${i + 1}`, ...portTextAttrs } }
|
||||
});
|
||||
}
|
||||
// Restore edge connections
|
||||
@@ -137,23 +143,23 @@ const CategoryList: FC<CategoryListProps> = ({ parentName, selectedNode, graphRe
|
||||
return (
|
||||
<Form.List name={parentName}>
|
||||
{(fields, { add, remove }) => (
|
||||
<Space direction="vertical" size={12} className="rb:w-full">
|
||||
<Flex gap={8} vertical>
|
||||
{fields.map(({ key, name, ...restField }, index) => {
|
||||
const currentItem = formValues?.[key] || {};
|
||||
const contentLength = (currentItem.class_name || '').length;
|
||||
|
||||
return (
|
||||
<div key={key} className="rb:border rb:border-[#DFE4ED] rb:rounded-md rb:p-2 rb:bg-[#F8F9FB]">
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mb-2">
|
||||
<div className="rb:text-[12px] rb:font-medium rb:py-1 rb:leading-2">{t('workflow.config.question-classifier.class_name')} {index + 1}</div>
|
||||
<div className="rb:flex rb:items-center rb:gap-1">
|
||||
<span className="rb:text-xs rb:text-gray-500">{contentLength}</span>
|
||||
<div key={key} className="rb-border rb:rounded-md rb:p-2">
|
||||
<Flex align="center" justify="space-between" className="rb:mb-2!">
|
||||
<div className="rb:text-[12px] rb:font-medium rb:py-1 rb:leading-4">{t('workflow.config.question-classifier.class_name')} {index + 1}</div>
|
||||
<Flex align="center" gap={4}>
|
||||
<span className="rb:text-xs rb:text-[#667085]">{contentLength}</span>
|
||||
<div
|
||||
className="rb:ml-1 rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/workflow/deleteBg.svg')] rb:hover:bg-[url('@/assets/images/workflow/deleteBg_hover.svg')]"
|
||||
onClick={() => handleRemoveCategory(remove, name, index)}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 'class_name']}
|
||||
@@ -177,7 +183,7 @@ const CategoryList: FC<CategoryListProps> = ({ parentName, selectedNode, graphRe
|
||||
>
|
||||
+ {t('workflow.config.question-classifier.addClassName')}
|
||||
</Button>
|
||||
</Space>
|
||||
</Flex>
|
||||
)}
|
||||
</Form.List>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { type FC, type ReactNode } from 'react';
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Button, Form, Input, Divider, Space, Select } from 'antd';
|
||||
import { Button, Form, Input, Flex, Space, Select } from 'antd';
|
||||
|
||||
interface OutputListProps {
|
||||
label: string;
|
||||
@@ -25,7 +25,7 @@ const OutputList: FC<OutputListProps> = ({ label, name, extra }) => {
|
||||
<Form.List name={name}>
|
||||
{(fields, { add, remove }) => (
|
||||
<>
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mb-2">
|
||||
<Flex align="center" justify="space-between" className="rb:mb-2!">
|
||||
<div className="rb:text-[12px] rb:font-medium rb:leading-4.5">
|
||||
{label}
|
||||
</div>
|
||||
@@ -34,15 +34,15 @@ const OutputList: FC<OutputListProps> = ({ label, name, extra }) => {
|
||||
{extra}
|
||||
<Button
|
||||
onClick={() => add({ type: 'string' })}
|
||||
className="rb:py-0! rb:px-1! rb:text-[12px]!"
|
||||
className="rb:py-0! rb:px-1! rb:h-4.5! rb:rounded-sm! rb:text-[12px]!"
|
||||
size="small"
|
||||
>
|
||||
+ {t('workflow.config.addVariable')}
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</Flex>
|
||||
{fields.map(({ key, name, ...restField }) => (
|
||||
<div key={key} className="rb:flex rb:items-center rb:gap-1 rb:mb-2">
|
||||
<Flex key={key} align="center" gap={4} className="rb:mb-2!">
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 'name']}
|
||||
@@ -51,7 +51,7 @@ const OutputList: FC<OutputListProps> = ({ label, name, extra }) => {
|
||||
<Input
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
size="small"
|
||||
className="rb:w-45!"
|
||||
className="rb:w-51!"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
@@ -67,14 +67,14 @@ const OutputList: FC<OutputListProps> = ({ label, name, extra }) => {
|
||||
}))}
|
||||
size="small"
|
||||
popupMatchSelectWidth={false}
|
||||
className="rb:w-22!"
|
||||
className="rb:w-27!"
|
||||
/>
|
||||
</Form.Item>
|
||||
<div
|
||||
className="rb:ml-1 rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/workflow/deleteBg.svg')] rb:hover:bg-[url('@/assets/images/workflow/deleteBg_hover.svg')]"
|
||||
onClick={() => remove(name)}
|
||||
></div>
|
||||
</div>
|
||||
</Flex>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.editor:global(.ant-select-single.ant-select-sm:not(.ant-select-customize-input) .ant-select-selector) {
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
import { type FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Form, Select, Space, Row, Col, Divider, Button, Tooltip } from 'antd'
|
||||
import { Form, Select, Flex, Tooltip } from 'antd'
|
||||
import { Node } from '@antv/x6'
|
||||
|
||||
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
||||
import MappingList from '../MappingList'
|
||||
import OutputList from './OutputList'
|
||||
import CodeMirrorEditor from '@/components/CodeMirrorEditor';
|
||||
import styles from './index.module.css'
|
||||
|
||||
interface MappingItem {
|
||||
name?: string
|
||||
@@ -73,40 +74,31 @@ const CodeExecution: FC<CodeExecutionProps> = ({ options }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form.Item name="input_variables" noStyle>
|
||||
<Form.Item name="input_variables">
|
||||
<MappingList
|
||||
label={t('workflow.config.code.input_variables')}
|
||||
name="input_variables"
|
||||
options={options}
|
||||
valueKey="variable"
|
||||
extra={<Tooltip title={t('workflow.config.code.refreshTip')}>
|
||||
<Button
|
||||
onClick={handleRefresh}
|
||||
className="rb:py-0! rb:px-1.5! rb:text-[12px]! rb:group"
|
||||
size="small"
|
||||
>
|
||||
<div onClick={handleRefresh} className="rb:size-3 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/refresh.svg')] rb:group-hover:bg-[url('@/assets/images/refresh_hover.svg')]"></div>
|
||||
</Button>
|
||||
<div onClick={handleRefresh} className="rb:size-4.5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/refresh.svg')]"></div>
|
||||
</Tooltip>}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Space size={8} direction="vertical" className="rb:w-full rb:border rb:border-[#DFE4ED] rb:rounded-md rb:px-2 rb:py-1.5">
|
||||
<Row>
|
||||
<Col span={12}>
|
||||
<Form.Item name="language" noStyle>
|
||||
<Flex gap={4} vertical className="rb:border rb:bg-[#F6F6F6] rb:border-[#F6F6F6] rb:hover:bg-white rb:hover:border-[#171719] rb:pr-2! rb:rounded-md rb:py-1.5! rb:mb-4!">
|
||||
<Form.Item name="language" noStyle className=" rb:px-2!">
|
||||
<Select
|
||||
options={[
|
||||
{ label: 'PYTHON3', value: 'python3' },
|
||||
{ label: 'JAVASCRIPT', value: 'javascript' }
|
||||
]}
|
||||
popupMatchSelectWidth={false}
|
||||
className="rb:font-medium!"
|
||||
className={`rb:font-medium! rb:w-25! rb:h-4! rb:p-0! ${styles.editor}`}
|
||||
onChange={handleChangeLanguage}
|
||||
variant="borderless"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Form.Item noStyle shouldUpdate={(prev, curr) => prev.language !== curr.language}>
|
||||
{() => (
|
||||
<Form.Item name="code" noStyle>
|
||||
@@ -117,9 +109,8 @@ const CodeExecution: FC<CodeExecutionProps> = ({ options }) => {
|
||||
</Form.Item>
|
||||
)}
|
||||
</Form.Item>
|
||||
</Space>
|
||||
</Flex>
|
||||
|
||||
<Divider />
|
||||
<Form.Item name="output_variables" noStyle>
|
||||
<OutputList
|
||||
label={t('workflow.config.code.output_variables')}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { type FC } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Form, Button, Select, InputNumber, Radio, Input, Divider, type SelectProps } from 'antd'
|
||||
import { Form, Button, Select, InputNumber, Input, Divider, type SelectProps, Flex, Space, Row, Col } from 'antd'
|
||||
|
||||
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
||||
import VariableSelect from '../VariableSelect'
|
||||
import RadioGroupBtn from '../RadioGroupBtn'
|
||||
|
||||
interface Case {
|
||||
logical_operator: 'and' | 'or';
|
||||
@@ -86,29 +87,40 @@ const ConditionList: FC<CaseListProps> = ({
|
||||
{(fields, { add, remove }) => {
|
||||
const logicalOperator = form.getFieldValue([parentName, 'logical_operator']);
|
||||
return (
|
||||
<div className="rb:relative">
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mb-2">
|
||||
<div className="rb:text-[12px] rb:font-medium rb:leading-4.5">
|
||||
<>
|
||||
<div className="rb:text-[12px] rb:mb-4!">
|
||||
<Flex align="center" justify="space-between" className="rb:mb-2!">
|
||||
<div className="rb:font-medium rb:leading-4.5">
|
||||
{t('workflow.config.loop.condition')}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={() => add({})}
|
||||
className="rb:py-0! rb:px-1! rb:text-[12px]!"
|
||||
className="rb:py-0! rb:px-1! rb:h-4.5! rb:rounded-sm! rb:text-[12px]!"
|
||||
size="small"
|
||||
>
|
||||
+ {t('workflow.config.loop.addCondition')}
|
||||
</Button>
|
||||
</div>
|
||||
{fields?.length > 1 && <div className="rb:absolute rb:top-8 rb:bottom-4 rb:w-8.5 rb:h-[calc(100%-32px)]">
|
||||
<div className="rb:absolute rb:w-2.5 rb:h-[calc(50%-30px)] rb:left-5 rb:top-4 rb:z-10 rb:border-l rb:border-t rb:border-[#DFE4ED] rb:rounded-tl-[10px] rb:border-r-0"></div>
|
||||
<div className="rb:absolute rb:z-10 rb:left-0 rb:top-[calc(50%-13px)]">
|
||||
</Flex>
|
||||
<div
|
||||
className={clsx("rb:relative", {
|
||||
'rb:ml-15!': fields?.length > 1
|
||||
})}
|
||||
>
|
||||
{fields?.length > 1 && (
|
||||
<div className="rb:absolute rb:-left-9 rb:top-4 rb:bottom-4 rb:w-6 rb:h-[calc(100%-32px)]">
|
||||
<div className="rb:absolute rb:w-3 rb:h-[calc(50%-20px)] rb:left-5 rb:top-0 rb:z-10 rb:border-l rb:border-t rb:border-[#EBEBEB] rb:rounded-tl-[10px] rb:border-r-0"></div>
|
||||
<div className="rb:absolute rb:z-10 rb:-right-1.25 rb:top-[calc(50%-10px)]">
|
||||
<Form.Item name={[parentName, 'logical_operator']} noStyle >
|
||||
<Button size="small" className="rb:text-[12px]! rb:py-px! rb:px-1! rb:w-8.5! rb:h-5!" onClick={handleChangeLogicalOperator}>{logicalOperator}</Button>
|
||||
<Space size={2} className="rb:cursor-pointer rb:text-[#155EEF] rb:leading-4.5 rb:font-medium rb-border rb:py-px! rb:px-1! rb:rounded-sm" onClick={handleChangeLogicalOperator}>
|
||||
{logicalOperator}
|
||||
<div className="rb:size-3 rb:bg-cover rb:bg-[url('src/assets/images/workflow/refresh_active.svg')]"></div>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</div>
|
||||
<div className="rb:absolute rb:w-2.5 rb:h-[calc(50%-30px)] rb:left-5 rb:bottom-4 rb:z-10 rb:border-l rb:border-b rb:border-[#DFE4ED] rb:rounded-bl-[10px] rb:border-r-0"></div>
|
||||
</div>}
|
||||
<div className="rb:absolute rb:w-3 rb:h-[calc(50%-20px)] rb:left-5 rb:bottom-0 rb:z-10 rb:border-l rb:border-b rb:border-[#EBEBEB] rb:rounded-bl-[10px] rb:border-r-0"></div>
|
||||
</div>
|
||||
)}
|
||||
{fields.map((field, index) => {
|
||||
const expressions = form.getFieldValue([parentName, 'expressions']) || [];
|
||||
const currentExpression = expressions[index] || {};
|
||||
@@ -118,15 +130,19 @@ const ConditionList: FC<CaseListProps> = ({
|
||||
const leftFieldOption = options.find(option => `{{${option.value}}}` === leftFieldValue);
|
||||
const leftFieldType = leftFieldOption?.dataType;
|
||||
const operatorList = operatorsObj[leftFieldType || 'default'] || operatorsObj.default || [];
|
||||
const inputType = leftFieldType === 'number' ? currentExpression.input_type?.toLocaleLowerCase() : undefined;
|
||||
console.log('inputType', inputType)
|
||||
|
||||
const inputType = leftFieldType === 'number' ? currentExpression.input_type : undefined;
|
||||
return (
|
||||
<div key={field.key} className="rb:flex rb:items-start rb:ml-9.5 rb:mb-4">
|
||||
<div className="rb:flex-1 rb:bg-[#F6F8FC] rb:border rb:border-[#DFE4ED] rb:rounded-md">
|
||||
<div className={clsx("rb:flex rb:gap-1 rb:p-1", {
|
||||
'rb:border-b rb:border-b-[#DFE4ED]': !hideRightField
|
||||
<Flex
|
||||
key={field.key}
|
||||
gap={4}
|
||||
align="start"
|
||||
className="rb:mb-2!"
|
||||
>
|
||||
<div className="rb:flex-1 rb:bg-[#F6F6F6] rb:rounded-lg">
|
||||
<Row className={clsx("rb:p-1!", {
|
||||
'rb-border-b': !hideRightField
|
||||
})}>
|
||||
<Col flex="1">
|
||||
<Form.Item name={[field.name, 'left']} noStyle>
|
||||
<VariableSelect
|
||||
options={options.filter(vo =>
|
||||
@@ -140,8 +156,12 @@ const ConditionList: FC<CaseListProps> = ({
|
||||
popupMatchSelectWidth={false}
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
onChange={(val) => handleLeftFieldChange(index, val)}
|
||||
variant="borderless"
|
||||
className="rb:w-full!"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col flex="96px">
|
||||
<Form.Item name={[field.name, 'operator']} noStyle>
|
||||
<Select
|
||||
options={operatorList.map(vo => ({
|
||||
@@ -151,64 +171,75 @@ const ConditionList: FC<CaseListProps> = ({
|
||||
size="small"
|
||||
popupMatchSelectWidth={false}
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
variant="borderless"
|
||||
className="rb:w-full!"
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{!hideRightField && <div className="rb:p-1">
|
||||
{!hideRightField && (
|
||||
<div className="rb:py-1 rb:px-1.5">
|
||||
{leftFieldType === 'number'
|
||||
? <div className="rb:flex rb:items-center">
|
||||
? (
|
||||
<Flex align="center">
|
||||
<Form.Item name={[field.name, 'input_type']} noStyle>
|
||||
<Select
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={[{ value: 'variable', label: 'Variable' }, { value: 'constant', label: 'Constant' }]}
|
||||
popupMatchSelectWidth={false}
|
||||
variant="borderless"
|
||||
className="rb:w-full!"
|
||||
className="rb:w-20!"
|
||||
onChange={() => handleInputTypeChange(index)}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Divider type="vertical" />
|
||||
<Form.Item name={[field.name, 'right']} noStyle>
|
||||
{inputType === 'variable'
|
||||
?
|
||||
{inputType === 'Variable'
|
||||
? (
|
||||
<VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={options.filter(vo => vo.dataType === 'number')}
|
||||
allowClear={false}
|
||||
popupMatchSelectWidth={false}
|
||||
variant="borderless"
|
||||
className="rb:w-full!"
|
||||
size="small"
|
||||
/>
|
||||
: <InputNumber
|
||||
)
|
||||
: (
|
||||
<InputNumber
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
variant="borderless"
|
||||
className="rb:w-full!"
|
||||
onChange={(value) => form.setFieldValue([parentName, 'expressions', index, 'right'], value)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</Form.Item>
|
||||
</div>
|
||||
: <Form.Item name={[field.name, 'right']} noStyle>
|
||||
</Flex>
|
||||
)
|
||||
: (
|
||||
<Form.Item name={[field.name, 'right']} noStyle>
|
||||
{leftFieldType === 'boolean'
|
||||
? <Radio.Group block>
|
||||
<Radio.Button value={true}>True</Radio.Button>
|
||||
<Radio.Button value={false}>False</Radio.Button>
|
||||
</Radio.Group>
|
||||
: <Input placeholder={t('common.pleaseEnter')} />
|
||||
? <RadioGroupBtn options={[ { value: true, label: 'True' }, { value: false, label: 'False' }]} />
|
||||
: <Input variant="borderless" placeholder={t('common.pleaseEnter')} />
|
||||
}
|
||||
</Form.Item>
|
||||
)
|
||||
}
|
||||
</div>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="rb:ml-1 rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/workflow/deleteBg.svg')] rb:hover:bg-[url('@/assets/images/workflow/deleteBg_hover.svg')]"
|
||||
className="rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/workflow/deleteBg.svg')] rb:hover:bg-[url('@/assets/images/workflow/deleteBg_hover.svg')]"
|
||||
onClick={() => remove(field.name)}
|
||||
></div>
|
||||
</div>
|
||||
</Flex>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}}
|
||||
</Form.List>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.select:global(.ant-select-single.ant-select-sm .ant-select-selector) {
|
||||
background: #F6F6F6;
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import { type FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Form, Select, Input, Button, InputNumber, Radio } from 'antd'
|
||||
import VariableSelect from '../VariableSelect'
|
||||
import { Form, Select, Input, Button, InputNumber, Flex } from 'antd'
|
||||
|
||||
import VariableSelect from '../VariableSelect'
|
||||
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
||||
import RadioGroupBtn from '../RadioGroupBtn'
|
||||
|
||||
interface CycleVar {
|
||||
name: string;
|
||||
@@ -81,27 +82,33 @@ const CycleVarsList: FC<CycleVarsListProps> = ({
|
||||
return (
|
||||
<Form.List name={parentName}>
|
||||
{(fields, { add, remove }) => (
|
||||
<>
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mb-3">
|
||||
<Flex vertical gap={8}>
|
||||
<Flex align="center" justify="space-between">
|
||||
<span className="rb:text-[12px] rb:font-medium">{t('workflow.config.loop.cycle_vars')}</span>
|
||||
<Button
|
||||
onClick={() => add({ name: '', type: 'string', input_type: 'constant', value: '' })}
|
||||
className="rb:py-0! rb:px-1! rb:text-[12px]!"
|
||||
className="rb:py-0! rb:px-1! rb:h-4.5! rb:rounded-sm! rb:text-[12px]!"
|
||||
size="small"
|
||||
>
|
||||
+ {t('workflow.config.addVariable')}
|
||||
</Button>
|
||||
</div>
|
||||
</Flex>
|
||||
<Flex vertical gap={12}>
|
||||
{fields.map(({ key, name }, index) => {
|
||||
const currentType = value?.[index]?.type;
|
||||
const currentInputType = value?.[index]?.input_type;
|
||||
|
||||
return (
|
||||
<div key={key} className="rb:flex rb:items-start rb:mb-2">
|
||||
<div className="rb:flex-1 rb:bg-[#F6F8FC] rb:border rb:border-[#DFE4ED] rb:rounded-md">
|
||||
<div className="rb:flex rb:gap-1 rb:p-1 rb:border-b rb:border-b-[#DFE4ED]">
|
||||
<Flex key={key} gap={4}>
|
||||
<Flex vertical gap={4}>
|
||||
<Flex gap={4}>
|
||||
<Form.Item name={[name, 'name']} noStyle>
|
||||
<Input size={size} className="rb:w-23!" placeholder={t('common.pleaseEnter')} />
|
||||
<Input
|
||||
size={size}
|
||||
className="rb:w-25.5! rb:bg-[#F6F6F6]!"
|
||||
variant="borderless"
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name={[name, 'type']} noStyle>
|
||||
<Select
|
||||
@@ -111,7 +118,8 @@ const CycleVarsList: FC<CycleVarsListProps> = ({
|
||||
}))}
|
||||
size={size}
|
||||
popupMatchSelectWidth={false}
|
||||
className="rb:w-18.5!"
|
||||
className={`rb:w-25.5! select`}
|
||||
variant="borderless"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name={[name, 'input_type']} noStyle>
|
||||
@@ -126,12 +134,13 @@ const CycleVarsList: FC<CycleVarsListProps> = ({
|
||||
onChange={() => {
|
||||
form.setFieldValue([parentName, index, 'value'], undefined);
|
||||
}}
|
||||
className="rb:w-18!"
|
||||
className={`rb:w-25! select`}
|
||||
variant="borderless"
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
</Flex>
|
||||
|
||||
<Form.Item name={[name, 'value']} noStyle>
|
||||
<Form.Item name={[name, 'value']} noStyle >
|
||||
{currentInputType === 'variable'
|
||||
? (
|
||||
<VariableSelect
|
||||
@@ -144,38 +153,42 @@ const CycleVarsList: FC<CycleVarsListProps> = ({
|
||||
})}
|
||||
variant="borderless"
|
||||
size="small"
|
||||
className="select"
|
||||
/>
|
||||
)
|
||||
: currentType === 'number'
|
||||
? <InputNumber
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
variant="borderless"
|
||||
className="rb:w-full! rb:my-1!"
|
||||
className="rb:w-full! rb:bg-[#F6F6F6]!"
|
||||
onChange={(value) => form.setFieldValue([name, 'value'], value)}
|
||||
/>
|
||||
: currentType === 'boolean'
|
||||
? <Radio.Group block>
|
||||
<Radio.Button value={true}>True</Radio.Button>
|
||||
<Radio.Button value={false}>False</Radio.Button>
|
||||
</Radio.Group>
|
||||
? <RadioGroupBtn
|
||||
options={[
|
||||
{ value: true, label: 'True' },
|
||||
{ value: false, label: 'False' }]}
|
||||
/>
|
||||
: (
|
||||
<Input.TextArea
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
rows={3}
|
||||
className="rb:w-full"
|
||||
className="rb:w-full rb:bg-[#F6F6F6]!"
|
||||
variant="borderless"
|
||||
/>
|
||||
)}
|
||||
)
|
||||
}
|
||||
</Form.Item>
|
||||
</div>
|
||||
</Flex>
|
||||
<div
|
||||
className="rb:ml-1 rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/workflow/deleteBg.svg')] rb:hover:bg-[url('@/assets/images/workflow/deleteBg_hover.svg')]"
|
||||
className="rb:mt-1.5 rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/workflow/deleteBg.svg')] rb:hover:bg-[url('@/assets/images/workflow/deleteBg_hover.svg')]"
|
||||
onClick={() => remove(name)}
|
||||
></div>
|
||||
</div>
|
||||
</Flex>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
</Flex>
|
||||
</Flex>
|
||||
)}
|
||||
</Form.List>
|
||||
)
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 15:17:39
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 15:17:39
|
||||
* @Last Modified time: 2026-03-03 10:54:15
|
||||
*/
|
||||
import { useEffect, type FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Form, Input, Button, Row, Col } from 'antd'
|
||||
import { Form, Input, Button, Flex } from 'antd'
|
||||
|
||||
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
||||
import VariableSelect from '../VariableSelect'
|
||||
@@ -126,8 +126,9 @@ const GroupVariableList: FC<GroupVariableListProps> = ({
|
||||
{fields.map(({ key, name, ...restField }) => {
|
||||
return (
|
||||
<div key={key} className="rb:mb-4">
|
||||
<Row gutter={12} className="rb:mb-2!">
|
||||
<Col span={12}>
|
||||
<Flex justify="space-between" className="rb:mb-0.5!">
|
||||
<Flex align="center" gap={4}>
|
||||
<div className="rb:size-4 rb:bg-cover rb:bg-[url('@/assets/images/workflow/file_fold.svg')]"></div>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={isCanAdd ? [name, 'key'] : undefined}
|
||||
@@ -136,17 +137,25 @@ const GroupVariableList: FC<GroupVariableListProps> = ({
|
||||
]}
|
||||
noStyle
|
||||
>
|
||||
{isCanAdd ? <Input placeholder={t('common.pleaseEnter')} size={size} /> : t('workflow.config.var-aggregator.variable')}
|
||||
{isCanAdd
|
||||
? <Input
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
size={size}
|
||||
variant="borderless"
|
||||
className="rb:border! rb:border-transparent! rb:py-px! rb:px-1! rb:rounded-md! rb:leading-4.25! rb:w-auto! rb:hover:bg-[#EBEBEB]! rb:hover:border-[#EBEBEB]! rb:focus:bg-transparent!"
|
||||
/>
|
||||
: t('workflow.config.var-aggregator.variable')
|
||||
}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Flex>
|
||||
|
||||
{isCanAdd && <Col span={12} className="rb:flex! rb:items-center rb:justify-end">
|
||||
{isCanAdd && (
|
||||
<div
|
||||
className="rb:ml-1 rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/workflow/deleteBg.svg')] rb:hover:bg-[url('@/assets/images/workflow/deleteBg_hover.svg')]"
|
||||
className="rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/workflow/deleteBg.svg')] rb:hover:bg-[url('@/assets/images/workflow/deleteBg_hover.svg')]"
|
||||
onClick={() => remove(name)}
|
||||
></div>
|
||||
</Col>}
|
||||
</Row>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
<Form.Item
|
||||
{...restField}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { type FC, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Button, Select, Table, Form, type TableProps } from 'antd';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { Button, Select, Table, Form, type TableProps, Flex } from 'antd';
|
||||
|
||||
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin';
|
||||
import Empty from '@/components/Empty';
|
||||
@@ -48,22 +47,21 @@ const EditableTable: FC<EditableTableProps> = ({
|
||||
|
||||
const getColumns = (remove: (index: number) => void): TableProps<TableRow>['columns'] => {
|
||||
const hasType = typeOptions.length > 0;
|
||||
const cellClassName="rb:p-1!"
|
||||
const contentClassName ="rb:w-[108px]! rb:text-[12px]! rb:overflow-hidden!"
|
||||
const contentClassName = hasType ? 'rb:w-[120px]!' : "rb:w-[154px]!"
|
||||
const formClassName = 'rb:mb-0! rb:bg-[#F6F6F6] rb:rounded-[8px] rb:py-[2px]! rb:px-[6px]!'
|
||||
|
||||
return [
|
||||
{
|
||||
title: t('workflow.config.name'),
|
||||
dataIndex: 'name',
|
||||
className: cellClassName,
|
||||
render: (_: any, __: TableRow, index: number) => (
|
||||
<Form.Item name={[index, 'name']} noStyle>
|
||||
<Form.Item name={[index, 'name']} className={formClassName}>
|
||||
<Editor
|
||||
options={booleanFilterOptions.filter(option => !option.dataType.includes('file'))}
|
||||
type="input"
|
||||
className={contentClassName}
|
||||
size={size}
|
||||
height={16}
|
||||
variant="borderless"
|
||||
/>
|
||||
</Form.Item>
|
||||
)
|
||||
@@ -72,7 +70,6 @@ const EditableTable: FC<EditableTableProps> = ({
|
||||
title: t('workflow.config.type'),
|
||||
dataIndex: 'type',
|
||||
width: '20%',
|
||||
className: cellClassName,
|
||||
render: (_: any, __: TableRow, index: number) => (
|
||||
<Form.Item shouldUpdate noStyle>
|
||||
{(form) => (
|
||||
@@ -86,6 +83,8 @@ const EditableTable: FC<EditableTableProps> = ({
|
||||
form.setFieldValue([...Array.isArray(parentName) ? parentName : [parentName], index, 'value'], undefined);
|
||||
}}
|
||||
size={size}
|
||||
variant="borderless"
|
||||
className="rb:w-17! select"
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
@@ -95,7 +94,6 @@ const EditableTable: FC<EditableTableProps> = ({
|
||||
{
|
||||
title: t('workflow.config.value'),
|
||||
dataIndex: 'value',
|
||||
className: cellClassName,
|
||||
render: (_: any, __: TableRow, index: number) => (
|
||||
<Form.Item
|
||||
shouldUpdate={(prevValues, currentValues) => {
|
||||
@@ -112,13 +110,13 @@ const EditableTable: FC<EditableTableProps> = ({
|
||||
: booleanFilterOptions.filter(option => !option.dataType.includes('file'));
|
||||
|
||||
return (
|
||||
<Form.Item name={[index, 'value']} noStyle>
|
||||
<Form.Item name={[index, 'value']} className={formClassName}>
|
||||
<Editor
|
||||
options={filteredOptions}
|
||||
type="input"
|
||||
className={contentClassName}
|
||||
size={size}
|
||||
height={16}
|
||||
variant="borderless"
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
@@ -129,10 +127,9 @@ const EditableTable: FC<EditableTableProps> = ({
|
||||
{
|
||||
title: '',
|
||||
dataIndex: 'actions',
|
||||
className: cellClassName,
|
||||
render: (_: any, __: TableRow, index: number) => (
|
||||
<div
|
||||
className="rb:ml-1 rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/workflow/deleteBg.svg')] rb:hover:bg-[url('@/assets/images/workflow/deleteBg_hover.svg')]"
|
||||
className="rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/workflow/deleteBg.svg')] rb:hover:bg-[url('@/assets/images/workflow/deleteBg_hover.svg')]"
|
||||
onClick={() => remove(index)}
|
||||
></div>
|
||||
)
|
||||
@@ -146,27 +143,26 @@ const EditableTable: FC<EditableTableProps> = ({
|
||||
{(fields, { add, remove }) => {
|
||||
const AddButton = ({ block = false }: { block?: boolean }) => (
|
||||
<Button
|
||||
icon={block ? undefined : <PlusOutlined />}
|
||||
onClick={() => add(createNewRow())}
|
||||
size="small"
|
||||
block={block}
|
||||
className={block ? "rb:mt-1 rb:text-[12px]! rb:bg-transparent!" : "rb:text-[12px]!"}
|
||||
className={block ? "rb:mt-2 rb:text-[12px]! rb:bg-transparent! rb:rounded-md" : "rb:text-[12px]! rb:rounded-sm!"}
|
||||
>
|
||||
{block && `+${t('common.add')}`}
|
||||
+ {t('common.add')}
|
||||
</Button>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{title && (
|
||||
<div className="rb:flex rb:items-center rb:mb-2 rb:justify-between">
|
||||
<Flex align="center" justify="space-between" className="rb:mb-2!">
|
||||
<div className="rb:font-medium rb:text-[12px] rb:leading-4.5">{title}</div>
|
||||
<AddButton block={false} />
|
||||
</div>
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
<Table<TableRow>
|
||||
bordered
|
||||
bordered={false}
|
||||
dataSource={fields.map((field) => ({
|
||||
key: String(field.key),
|
||||
name: undefined,
|
||||
@@ -176,9 +172,7 @@ const EditableTable: FC<EditableTableProps> = ({
|
||||
columns={getColumns(remove)}
|
||||
pagination={false}
|
||||
size="small"
|
||||
rowClassName="rb:p-0! rb:bg-[#F6F8FC]!"
|
||||
locale={{ emptyText: <Empty size={88} /> }}
|
||||
style={{ width: '274px' }}
|
||||
/>
|
||||
|
||||
{!title && <AddButton block />}
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-09 18:35:43
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-02 17:24:51
|
||||
* @Last Modified time: 2026-03-04 15:20:32
|
||||
*/
|
||||
import { type FC, useRef, useState } from "react";
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Form, Row, Col, Select, Button, Divider, InputNumber, Switch, Input } from 'antd'
|
||||
import { Form, Row, Col, Select, Button, Divider, InputNumber, Switch, Input, Flex, Radio } from 'antd'
|
||||
import { CaretDownOutlined, CaretRightOutlined, SettingOutlined } from '@ant-design/icons';
|
||||
|
||||
import Editor from '../../Editor'
|
||||
@@ -15,7 +15,7 @@ import AuthConfigModal from './AuthConfigModal'
|
||||
import type { AuthConfigModalRef, HttpRequestConfigForm } from './types'
|
||||
import MessageEditor from '../MessageEditor'
|
||||
import EditableTable from './EditableTable'
|
||||
import { portTextAttrs } from '../../../constant'
|
||||
import { portTextAttrs, nodeWidth, portItemArgsY } from '../../../constant'
|
||||
|
||||
const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: any; }> = ({
|
||||
options,
|
||||
@@ -35,8 +35,9 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
|
||||
form.setFieldsValue({ auth })
|
||||
}
|
||||
|
||||
const handleChangeBodyContentType = () => {
|
||||
form.setFieldValue(['body', 'data'], undefined)
|
||||
const handleChangeBodyContentType = (e: any) => {
|
||||
const value = e.target.value || e.target.value
|
||||
form.setFieldValue(['body', 'data'], ['form-data', 'x-www-form-urlencoded'].includes(value) ? [{}] : undefined)
|
||||
}
|
||||
|
||||
// Handle error handling method change and update node ports accordingly
|
||||
@@ -61,6 +62,10 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
|
||||
selectedNode.addPort({
|
||||
id: 'ERROR',
|
||||
group: 'right',
|
||||
args: {
|
||||
x: nodeWidth,
|
||||
y: portItemArgsY + portItemArgsY,
|
||||
},
|
||||
attrs: { text: { text: t('workflow.config.http-request.errorBranch'), ...portTextAttrs }}
|
||||
});
|
||||
} else if (method !== 'branch' && errorPort) {
|
||||
@@ -81,7 +86,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mb-1">
|
||||
<Flex align="center" justify="space-between" className="rb:mb-1!">
|
||||
<div className="rb:font-medium rb:text-[12px] rb:leading-4.5">API</div>
|
||||
<Button onClick={handleChangeAuth}
|
||||
size="small"
|
||||
@@ -89,7 +94,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
|
||||
icon={<SettingOutlined />}
|
||||
className="rb:mt-1 rb:text-[12px]!"
|
||||
>{t('workflow.config.http-request.auth')}: {!values?.auth?.auth_type || values?.auth?.auth_type === 'none' ? t('workflow.config.http-request.none') : t('workflow.config.http-request.apiKey')}</Button>
|
||||
</div>
|
||||
</Flex>
|
||||
<Row gutter={4}>
|
||||
<Col span={8}>
|
||||
<Form.Item name="method">
|
||||
@@ -113,6 +118,7 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
|
||||
variant="outlined"
|
||||
type="input"
|
||||
size="small"
|
||||
height={28}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
@@ -139,9 +145,9 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="BODY" className="rb:mb-0!">
|
||||
<Form.Item name={['body', 'content_type']}>
|
||||
<Select
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
<Form.Item name={['body', 'content_type']} className="rb:mb-3!">
|
||||
<Radio.Group
|
||||
size="small"
|
||||
onChange={handleChangeBodyContentType}
|
||||
options={[
|
||||
{ label: 'none', value: 'none' },
|
||||
@@ -184,6 +190,9 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
|
||||
options={options.filter(vo => vo.dataType === 'string' || vo.dataType === 'number')}
|
||||
isArray={false}
|
||||
title="JSON"
|
||||
titleVariant="borderless"
|
||||
size="small"
|
||||
className="rb:bg-[#F6F6F6] rb:border-[#F6F6F6]! rb:hover:bg-white rb:hover:border-[#171719]!"
|
||||
/>
|
||||
</Form.Item>
|
||||
}
|
||||
@@ -195,6 +204,9 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
|
||||
options={options.filter(vo => vo.dataType === 'string' || vo.dataType === 'number')}
|
||||
isArray={false}
|
||||
title="RAW TEXT"
|
||||
titleVariant="borderless"
|
||||
size="small"
|
||||
className="rb:bg-[#F6F6F6] rb:border-[#F6F6F6]! rb:hover:bg-white rb:hover:border-[#171719]!"
|
||||
/>
|
||||
</Form.Item>
|
||||
}
|
||||
@@ -221,8 +233,9 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
|
||||
</div>
|
||||
<Form.Item
|
||||
name={['timeouts', 'connect_timeout']}
|
||||
label={t('workflow.config.http-request.connect_timeout')}
|
||||
label={<span className="rb:text-[#5B6167]">{t('workflow.config.http-request.connect_timeout')}</span>}
|
||||
hidden={collapsed}
|
||||
className="rb:mb-2!"
|
||||
>
|
||||
<InputNumber
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
@@ -232,8 +245,9 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={['timeouts', 'read_timeout']}
|
||||
label={t('workflow.config.http-request.read_timeout')}
|
||||
label={<span className="rb:text-[#5B6167]">{t('workflow.config.http-request.read_timeout')}</span>}
|
||||
hidden={collapsed}
|
||||
className="rb:mb-2!"
|
||||
>
|
||||
<InputNumber
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
@@ -243,8 +257,9 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={['timeouts', 'write_timeout']}
|
||||
label={t('workflow.config.http-request.write_timeout')}
|
||||
label={<span className="rb:text-[#5B6167]">{t('workflow.config.http-request.write_timeout')}</span>}
|
||||
hidden={collapsed}
|
||||
className="rb:mb-2!"
|
||||
>
|
||||
<InputNumber
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
@@ -261,7 +276,8 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
|
||||
<>
|
||||
<Form.Item
|
||||
name={['retry', 'max_attempts']}
|
||||
label={t('workflow.config.http-request.max_attempts')}
|
||||
label={<span className="rb:text-[#5B6167]">{t('workflow.config.http-request.max_attempts')}</span>}
|
||||
className="rb:mb-2!"
|
||||
>
|
||||
<InputNumber
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
@@ -271,7 +287,8 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={['retry', 'retry_interval']}
|
||||
label={<>{t('workflow.config.http-request.retry_interval')} <span className="rb:text-[#5B6167]">(ms)</span></>}
|
||||
label={<span className="rb:text-[#5B6167]">{t('workflow.config.http-request.retry_interval')}(ms)</span>}
|
||||
className="rb:mb-2!"
|
||||
>
|
||||
<InputNumber
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
@@ -283,7 +300,9 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
|
||||
}
|
||||
|
||||
<Divider />
|
||||
<Form.Item layout="horizontal" name={['error_handle', 'method']} label={t('workflow.config.http-request.error_handle')}>
|
||||
<Flex justify="space-between" align="center">
|
||||
<div className="rb:text-[12px] rb:font-medium">{t('workflow.config.http-request.error_handle')}</div>
|
||||
<Form.Item layout="horizontal" name={['error_handle', 'method']} noStyle>
|
||||
<Select
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
onChange={handleChangeErrorHandleMethod}
|
||||
@@ -292,19 +311,29 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
|
||||
{ value: 'default', label: t('workflow.config.http-request.default') },
|
||||
{ value: 'branch', label: t('workflow.config.http-request.branch') },
|
||||
]}
|
||||
className="rb:w-30!"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Flex>
|
||||
{values?.error_handle?.method === 'default' &&
|
||||
<>
|
||||
<Form.Item
|
||||
name={['error_handle', 'body']}
|
||||
label={<>body <span className="rb:text-[#5B6167] rb:ml-1">string</span></>}
|
||||
label={<>
|
||||
<span className="rb:text-[#5B6167] rb:font-medium">body</span>
|
||||
<span className="rb:text-[#5B6167] rb:ml-1" style={{fontWeight: 400}}>string</span>
|
||||
</>}
|
||||
className="rb:my-2!"
|
||||
>
|
||||
<Input placeholder={t('common.pleaseEnter')} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={['error_handle', 'status_code']}
|
||||
label={<>status_code <span className="rb:text-[#5B6167] rb:ml-1">number</span></>}
|
||||
label={<>
|
||||
<span className="rb:text-[#5B6167] rb:font-medium">status_code</span>
|
||||
<span className="rb:text-[#5B6167] rb:ml-1" style={{fontWeight: 400}}>number</span>
|
||||
</>}
|
||||
className="rb:my-2!"
|
||||
>
|
||||
<InputNumber
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
@@ -314,12 +343,17 @@ const HttpRequest: FC<{ options: Suggestion[]; selectedNode?: any; graphRef?: an
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={['error_handle', 'headers']}
|
||||
label={<>headers <span className="rb:text-[#5B6167] rb:ml-1">object</span></>}
|
||||
label={<>
|
||||
<span className="rb:text-[#5B6167] rb:font-medium">headers</span>
|
||||
<span className="rb:text-[#5B6167] rb:ml-1" style={{fontWeight: 400}}>object</span>
|
||||
</>}
|
||||
className="rb:my-2!"
|
||||
>
|
||||
<Input.TextArea placeholder={t('common.pleaseEnter')} />
|
||||
</Form.Item>
|
||||
</>
|
||||
}
|
||||
<Divider />
|
||||
|
||||
<AuthConfigModal
|
||||
ref={authConfigModalRef}
|
||||
|
||||
@@ -174,7 +174,7 @@ const JinjaRender: FC<JinjaRenderProps> = ({ selectedNode, options, templateOpti
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form.Item name="mapping" noStyle>
|
||||
<Form.Item name="mapping">
|
||||
<MappingList label={t('workflow.config.jinja-render.mapping')} name="mapping" options={options} />
|
||||
</Form.Item>
|
||||
|
||||
@@ -188,6 +188,7 @@ const JinjaRender: FC<JinjaRenderProps> = ({ selectedNode, options, templateOpti
|
||||
options={templateOptions}
|
||||
titleVariant="borderless"
|
||||
size="small"
|
||||
className="rb:bg-[#F6F6F6] rb:border-[#F6F6F6]! rb:hover:bg-white rb:hover:border-[#171719]!"
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { type FC, useRef, useState, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Space, Button } from 'antd'
|
||||
import knowledgeEmpty from '@/assets/images/application/knowledgeEmpty.svg'
|
||||
import { Space, Button, Flex } from 'antd'
|
||||
|
||||
import type {
|
||||
KnowledgeConfigForm,
|
||||
KnowledgeConfig,
|
||||
@@ -11,7 +11,6 @@ import type {
|
||||
KnowledgeConfigModalRef,
|
||||
KnowledgeGlobalConfigModalRef,
|
||||
} from './types'
|
||||
import Empty from '@/components/Empty'
|
||||
import KnowledgeListModal from './KnowledgeListModal'
|
||||
import KnowledgeConfigModal from './KnowledgeConfigModal'
|
||||
import KnowledgeGlobalConfigModal from './KnowledgeGlobalConfigModal'
|
||||
@@ -113,7 +112,7 @@ const Knowledge: FC<{value?: KnowledgeConfig; onChange?: (config: KnowledgeConfi
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mb-2">
|
||||
<Flex align="center" justify="space-between" className="rb:mb-2!">
|
||||
<div className="rb:text-[12px] rb:font-medium rb:leading-4.5">
|
||||
{t('application.knowledgeBaseAssociation')}
|
||||
</div>
|
||||
@@ -122,15 +121,16 @@ const Knowledge: FC<{value?: KnowledgeConfig; onChange?: (config: KnowledgeConfi
|
||||
onClick={handleKnowledgeConfig}
|
||||
className="rb:py-0! rb:px-1! rb:text-[12px]! rb:group rb:gap-0.5!"
|
||||
size="small"
|
||||
disabled={knowledgeList.length === 0}
|
||||
>
|
||||
<div
|
||||
className="rb:size-3.5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/workflow/recall.svg')] rb:group-hover:bg-[url('@/assets/images/workflow/recall_hover.svg')]"
|
||||
className="rb:size-3.5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/workflow/recall.svg')]"
|
||||
></div>
|
||||
{t('application.globalConfig')}
|
||||
</Button>
|
||||
</div>
|
||||
</Flex>
|
||||
|
||||
<Space size={10} direction="vertical" className="rb:w-full!">
|
||||
<Flex gap={10} vertical>
|
||||
<Button
|
||||
type="dashed"
|
||||
block
|
||||
@@ -141,37 +141,35 @@ const Knowledge: FC<{value?: KnowledgeConfig; onChange?: (config: KnowledgeConfi
|
||||
+ {t('workflow.config.knowledge-retrieval.addKnowledge')}
|
||||
</Button>
|
||||
|
||||
{knowledgeList.length === 0
|
||||
? <Empty url={knowledgeEmpty} size={88} subTitle={t('application.knowledgeEmpty')} />
|
||||
: knowledgeList.map(item => {
|
||||
{knowledgeList.length > 0 && knowledgeList.map(item => {
|
||||
if (!item.id) return null
|
||||
return (
|
||||
<div key={item.id} className="rb:text-[12px] rb:flex rb:items-center rb:justify-between rb:py-2 rb:px-2.5 rb:bg-[#F6F8FC] rb:border rb:border-[#DFE4ED] rb:rounded-lg">
|
||||
<Flex key={item.id} align="center" justify="space-between" className="rb:text-[12px] rb:py-1.75! rb:px-2.5! rb-border rb:rounded-lg">
|
||||
<div className="">
|
||||
<span className="rb:font-medium rb:leading-4">{item.name}</span>
|
||||
<span className="rb:font-medium rb:leading-4.25">{item.name}</span>
|
||||
<Tag
|
||||
color={item.status === 1 ? 'success' : item.status === 0 ? 'default' : 'error'}
|
||||
className="rb:ml-1 rb:py-0! rb:px-1! rb:text-[12px] rb:leading-3.5!"
|
||||
className="rb:ml-1 rb:py-0! rb:px-1! rb:text-[12px] rb:leading-4!"
|
||||
>
|
||||
{item.status === 1 ? t('common.enable') : item.status === 0 ? t('common.disabled') : t('common.deleted')}
|
||||
</Tag>
|
||||
<div className="rb:mt-1 rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-5">{t('application.contains', { include_count: item.doc_num })}</div>
|
||||
<div className="rb:mt-1 rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-4.25">{t('application.contains', { include_count: item.doc_num })}</div>
|
||||
</div>
|
||||
<Space size={12}>
|
||||
<div
|
||||
className="rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/edit.svg')] rb:hover:bg-[url('@/assets/images/edit_hover.svg')]"
|
||||
className="rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/edit.svg')]"
|
||||
onClick={() => handleEditKnowledge(item)}
|
||||
></div>
|
||||
<div
|
||||
className="rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/delete.svg')] rb:hover:bg-[url('@/assets/images/delete_hover.svg')]"
|
||||
className="rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/delete.svg')]"
|
||||
onClick={() => handleDeleteKnowledge(item.id)}
|
||||
></div>
|
||||
</Space>
|
||||
</div>
|
||||
</Flex>
|
||||
)
|
||||
})
|
||||
}
|
||||
</Space>
|
||||
</Flex>
|
||||
{/* 全局设置 */}
|
||||
<KnowledgeGlobalConfigModal
|
||||
data={editConfig}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
|
||||
import { Form, Select, InputNumber } from 'antd';
|
||||
import { Form, Select, InputNumber, Flex } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { KnowledgeConfigModalRef, KnowledgeBase, KnowledgeConfigForm, RetrieveType } from './types'
|
||||
@@ -86,13 +86,13 @@ const KnowledgeConfigModal = forwardRef<KnowledgeConfigModalRef, KnowledgeConfig
|
||||
size="middle"
|
||||
>
|
||||
{data && (
|
||||
<div className="rb:mb-6 rb:flex rb:items-center rb:justify-between rb:border rb:rounded-lg rb:p-[17px_16px] rb:cursor-pointer rb:bg-[#F0F3F8] rb:border-[#DFE4ED] rb:text-[#212332]">
|
||||
<Flex align="center" justify="space-between" className="rb:mb-6! rb-border rb:rounded-lg rb:p-[17px_16px]! rb:cursor-pointer rb:bg-[#F0F3F8] rb:text-[#212332]">
|
||||
<div className="rb:text-[16px] rb:leading-5.5">
|
||||
{data.name}
|
||||
<div className="rb:text-[12px] rb:leading-4 rb:text-[#5B6167] rb:mt-2">{t('application.contains', {include_count: data.doc_num})}</div>
|
||||
</div>
|
||||
<div className="rb:text-[12px] rb:leading-4 rb:text-[#5B6167]">{formatDateTime(data.updated_at, 'YYYY-MM-DD HH:mm:ss')}</div>
|
||||
</div>
|
||||
</Flex>
|
||||
)}
|
||||
<FormItem name="kb_id" hidden />
|
||||
{/* 检索模式 */}
|
||||
@@ -137,6 +137,7 @@ const KnowledgeConfigModal = forwardRef<KnowledgeConfigModalRef, KnowledgeConfig
|
||||
max={1.0}
|
||||
step={0.1}
|
||||
min={0.0}
|
||||
isInput={true}
|
||||
/>
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -152,6 +153,7 @@ const KnowledgeConfigModal = forwardRef<KnowledgeConfigModalRef, KnowledgeConfig
|
||||
max={1.0}
|
||||
step={0.1}
|
||||
min={0.0}
|
||||
isInput={true}
|
||||
/>
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -168,6 +170,7 @@ const KnowledgeConfigModal = forwardRef<KnowledgeConfigModalRef, KnowledgeConfig
|
||||
max={1.0}
|
||||
step={0.1}
|
||||
min={0.0}
|
||||
isInput={true}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
@@ -180,6 +183,7 @@ const KnowledgeConfigModal = forwardRef<KnowledgeConfigModalRef, KnowledgeConfig
|
||||
max={1.0}
|
||||
step={0.1}
|
||||
min={0.0}
|
||||
isInput={true}
|
||||
/>
|
||||
</FormItem>
|
||||
</>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { forwardRef, useImperativeHandle, useState, useEffect } from 'react';
|
||||
import { Form, InputNumber, Switch } from 'antd';
|
||||
import { Form, InputNumber, Switch, Flex } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { RerankerConfig, KnowledgeGlobalConfigModalRef } from './types'
|
||||
@@ -75,7 +75,7 @@ const KnowledgeGlobalConfigModal = forwardRef<KnowledgeGlobalConfigModalRef, Kno
|
||||
<div className="rb:text-[#5B6167] rb:mb-6">{t('application.globalConfigDesc')}</div>
|
||||
|
||||
{/* 结果重排 */}
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:my-6">
|
||||
<Flex align="center" justify="space-between" className="rb:my-6!">
|
||||
<div className="rb:text-[14px] rb:font-medium rb:leading-5">
|
||||
{t('application.rerankModel')}
|
||||
<div className="rb:mt-1 rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-4">{t('application.rerankModelDesc')}</div>
|
||||
@@ -87,7 +87,7 @@ const KnowledgeGlobalConfigModal = forwardRef<KnowledgeGlobalConfigModalRef, Kno
|
||||
>
|
||||
<Switch />
|
||||
</FormItem>
|
||||
</div>
|
||||
</Flex>
|
||||
|
||||
{values?.rerank_model && <>
|
||||
<FormItem
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
|
||||
import { Space, List } from 'antd';
|
||||
import { List, Form, Flex } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import clsx from 'clsx'
|
||||
|
||||
import type { KnowledgeModalRef, KnowledgeBase } from './types'
|
||||
import type { KnowledgeBaseListItem } from '@/views/KnowledgeBase/types'
|
||||
import RbModal from '@/components/RbModal'
|
||||
@@ -22,21 +23,23 @@ const KnowledgeListModal = forwardRef<KnowledgeModalRef, KnowledgeModalProps>(({
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [list, setList] = useState<KnowledgeBaseListItem[]>([])
|
||||
const [filterList, setFilterList] = useState<KnowledgeBaseListItem[]>([])
|
||||
const [query, setQuery] = useState<{keywords?: string}>({})
|
||||
const [selectedIds, setSelectedIds] = useState<string[]>([])
|
||||
const [selectedRows, setSelectedRows] = useState<KnowledgeBase[]>([])
|
||||
|
||||
const [form] = Form.useForm()
|
||||
const query = Form.useWatch([], form)
|
||||
|
||||
// 封装取消方法,添加关闭弹窗逻辑
|
||||
const handleClose = () => {
|
||||
setVisible(false);
|
||||
setQuery({})
|
||||
form.resetFields()
|
||||
setSelectedIds([])
|
||||
setSelectedRows([])
|
||||
};
|
||||
|
||||
const handleOpen = () => {
|
||||
setVisible(true);
|
||||
setQuery({})
|
||||
form.resetFields()
|
||||
setSelectedIds([])
|
||||
setSelectedRows([])
|
||||
};
|
||||
@@ -45,7 +48,7 @@ const KnowledgeListModal = forwardRef<KnowledgeModalRef, KnowledgeModalProps>(({
|
||||
if (visible) {
|
||||
getList()
|
||||
}
|
||||
}, [query.keywords, visible])
|
||||
}, [query?.keywords, visible])
|
||||
const getList = () => {
|
||||
getKnowledgeBaseList(undefined, {
|
||||
...query,
|
||||
@@ -77,11 +80,6 @@ const KnowledgeListModal = forwardRef<KnowledgeModalRef, KnowledgeModalProps>(({
|
||||
handleOpen,
|
||||
handleClose
|
||||
}));
|
||||
const handleSearch = (value?: string) => {
|
||||
setQuery({keywords: value})
|
||||
setSelectedIds([])
|
||||
setSelectedRows([])
|
||||
}
|
||||
const handleSelect = (item: KnowledgeBase) => {
|
||||
const index = selectedIds.indexOf(item.id)
|
||||
if (index === -1) {
|
||||
@@ -112,22 +110,27 @@ const KnowledgeListModal = forwardRef<KnowledgeModalRef, KnowledgeModalProps>(({
|
||||
onOk={handleSave}
|
||||
width={1000}
|
||||
>
|
||||
<Space size={24} direction="vertical" className="rb:w-full">
|
||||
<Flex gap={24} vertical>
|
||||
<Form form={form}>
|
||||
<Form.Item name="keywords" noStyle>
|
||||
<SearchInput
|
||||
placeholder={t('knowledgeBase.searchPlaceholder')}
|
||||
onSearch={handleSearch}
|
||||
style={{ width: '100%' }}
|
||||
size="middle"
|
||||
className="rb:w-full!"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
{filterList.length === 0
|
||||
? <Empty />
|
||||
: <List
|
||||
grid={{ gutter: 16, column: 2 }}
|
||||
dataSource={filterList}
|
||||
renderItem={(item: KnowledgeBase) => (
|
||||
<List.Item>
|
||||
<div key={item.id} className={clsx("rb:flex rb:items-center rb:justify-between rb:border rb:rounded-lg rb:p-[17px_16px] rb:cursor-pointer rb:hover:bg-[#F0F3F8]", {
|
||||
"rb:bg-[rgba(21,94,239,0.06)] rb:border-[#155EEF] rb:text-[#155EEF]": selectedIds.includes(item.id),
|
||||
<List.Item key={item.id}>
|
||||
<Flex
|
||||
align="center"
|
||||
justify="space-between"
|
||||
className={clsx("rb:border rb:rounded-lg rb:p-[17px_16px]! rb:cursor-pointer rb:hover:bg-[#F0F3F8]", {
|
||||
"rb:bg-[rgba(21,94,239,0.06)] rb:border-[#171719] rb:text-[#171719]": selectedIds.includes(item.id),
|
||||
"rb:border-[#DFE4ED] rb:text-[#212332]": !selectedIds.includes(item.id),
|
||||
})} onClick={() => handleSelect(item)}>
|
||||
<div className="rb:text-[16px] rb:leading-5.5">
|
||||
@@ -135,12 +138,12 @@ const KnowledgeListModal = forwardRef<KnowledgeModalRef, KnowledgeModalProps>(({
|
||||
<div className="rb:text-[12px] rb:leading-4 rb:text-[#5B6167] rb:mt-2">{t('application.contains', {include_count: item.doc_num})}</div>
|
||||
</div>
|
||||
<div className="rb:text-[12px] rb:leading-4 rb:text-[#5B6167]">{formatDateTime(item.created_at, 'YYYY-MM-DD HH:mm:ss')}</div>
|
||||
</div>
|
||||
</Flex>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
}
|
||||
</Space>
|
||||
</Flex>
|
||||
</RbModal>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { type FC, type ReactNode } from 'react';
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Button, Form, Input, Divider, Space } from 'antd';
|
||||
import { Button, Form, Input, Space, Flex } from 'antd';
|
||||
|
||||
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
||||
import VariableSelect from '../VariableSelect'
|
||||
|
||||
@@ -18,7 +19,7 @@ const MappingList: FC<MappingListProps> = ({ label, name, options, extra, valueK
|
||||
<Form.List name={name}>
|
||||
{(fields, { add, remove }) => (
|
||||
<>
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mb-2">
|
||||
<Flex align="center" justify="space-between" className="rb:mb-2!">
|
||||
<div className="rb:text-[12px] rb:font-medium rb:leading-4.5">
|
||||
{label}
|
||||
</div>
|
||||
@@ -27,15 +28,16 @@ const MappingList: FC<MappingListProps> = ({ label, name, options, extra, valueK
|
||||
{extra}
|
||||
<Button
|
||||
onClick={() => add()}
|
||||
className="rb:py-0! rb:px-1! rb:text-[12px]!"
|
||||
className="rb:py-0! rb:px-1! rb:h-4.5! rb:rounded-sm! rb:text-[12px]!"
|
||||
size="small"
|
||||
>
|
||||
+ {t('workflow.config.addVariable')}
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</Flex>
|
||||
<Flex gap={8} vertical>
|
||||
{fields.map(({ key, name, ...restField }) => (
|
||||
<div key={key} className="rb:flex rb:items-center rb:gap-1 rb:mb-2">
|
||||
<Flex key={key} align="center" gap={4}>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 'name']}
|
||||
@@ -44,7 +46,7 @@ const MappingList: FC<MappingListProps> = ({ label, name, options, extra, valueK
|
||||
<Input
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
size="small"
|
||||
className="rb:w-24!"
|
||||
className="rb:w-27!"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
@@ -57,19 +59,19 @@ const MappingList: FC<MappingListProps> = ({ label, name, options, extra, valueK
|
||||
options={options}
|
||||
popupMatchSelectWidth={false}
|
||||
size="small"
|
||||
className="rb:w-39!"
|
||||
className="rb:w-51!"
|
||||
/>
|
||||
</Form.Item>
|
||||
<div
|
||||
className="rb:ml-1 rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/workflow/deleteBg.svg')] rb:hover:bg-[url('@/assets/images/workflow/deleteBg_hover.svg')]"
|
||||
className="rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/workflow/deleteBg.svg')] rb:hover:bg-[url('@/assets/images/workflow/deleteBg_hover.svg')]"
|
||||
onClick={() => remove(name)}
|
||||
></div>
|
||||
</div>
|
||||
</Flex>
|
||||
))}
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
<Divider />
|
||||
</>
|
||||
)
|
||||
};
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { type FC } from "react";
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Form, Row, Col, Divider, Switch, Slider } from 'antd'
|
||||
import { Form, Switch, Flex } from 'antd'
|
||||
|
||||
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
||||
import MessageEditor from '../MessageEditor'
|
||||
import RbSlider from "@/components/RbSlider";
|
||||
|
||||
const MemoryConfig: FC<{ options: Suggestion[]; parentName: string; }> = ({
|
||||
options,
|
||||
@@ -29,12 +31,15 @@ const MemoryConfig: FC<{ options: Suggestion[]; parentName: string; }> = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form.Item layout="horizontal" name={[parentName, 'enable']} label={t('workflow.config.llm.memory')} className={values?.memory?.enable ? "rb:mb-2!" : undefined}>
|
||||
<Switch onChange={handleChangeEnable} />
|
||||
</Form.Item>
|
||||
{values?.memory?.enable && <>
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:py-1.5 rb:px-2 rb:text-[12px] rb:bg-[#F6F8FC] rb:rounded-md rb:mb-2">
|
||||
<Flex align="center" justify="space-between" className="rb:py-1.25! rb:px-2! rb:text-[12px] rb:leading-4.5 rb:bg-[#F6F6F6] rb:rounded-lg rb:mb-2!">
|
||||
{t('workflow.config.llm.memory')}
|
||||
<span>{t('workflow.config.llm.inner')}</span>
|
||||
</div>
|
||||
<Form.Item layout="horizontal" name={[parentName, 'messages']}>
|
||||
</Flex>
|
||||
<Form.Item layout="horizontal" name={[parentName, 'messages']} className="rb:mb-2!">
|
||||
<MessageEditor
|
||||
title="USER"
|
||||
isArray={false}
|
||||
@@ -43,26 +48,21 @@ const MemoryConfig: FC<{ options: Suggestion[]; parentName: string; }> = ({
|
||||
size="small"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Divider />
|
||||
</>}
|
||||
<Form.Item layout="horizontal" name={[parentName, 'enable']} label={t('workflow.config.llm.memory')}>
|
||||
<Switch onChange={handleChangeEnable} />
|
||||
</Form.Item>
|
||||
{values?.memory?.enable && <>
|
||||
<Row className="rb:mb-3">
|
||||
<Col span={10}>
|
||||
<Form.Item layout="horizontal" name={[parentName, 'enable_window']} noStyle>
|
||||
<div className="rb-border rb:rounded-lg rb:p-2 rb:mb-4">
|
||||
<Form.Item layout="horizontal" name={[parentName, 'enable_window']} label={t('workflow.config.llm.enable_window')} className="rb:mb-2!">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<span className="rb:ml-2 rb:text-[12px]">{t('workflow.config.llm.enable_window')}</span>
|
||||
</Col>
|
||||
<Col span={14}>
|
||||
<Form.Item layout="horizontal" name={[parentName, 'window_size']} noStyle>
|
||||
<Slider min={1} max={100} step={1} className="rb:my-0!" disabled={!values?.memory?.enable_window} />
|
||||
<RbSlider
|
||||
min={1}
|
||||
max={100}
|
||||
step={1}
|
||||
size="small"
|
||||
isInput={true}
|
||||
disabled={!values?.memory?.enable_window}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</>}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { type FC, type ReactNode, useMemo } from 'react';
|
||||
import clsx from 'clsx'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Input, Form, Space, Button, Row, Col, Select, type FormListOperation } from 'antd';
|
||||
import { Input, Form, Button, Row, Col, Select, type FormListOperation, Flex } from 'antd';
|
||||
|
||||
import Editor, { type LexicalEditorProps } from '../Editor'
|
||||
import type { Suggestion } from '../Editor/plugin/AutocompletePlugin'
|
||||
|
||||
@@ -16,7 +17,8 @@ interface MessageEditor {
|
||||
value?: string;
|
||||
language?: LexicalEditorProps['language'];
|
||||
onChange?: (value?: string) => void;
|
||||
size?: 'small' | 'default'
|
||||
size?: 'small' | 'default';
|
||||
className?: string;
|
||||
}
|
||||
const roleOptions = [
|
||||
// { label: 'SYSTEM', value: 'SYSTEM' },
|
||||
@@ -31,7 +33,8 @@ const MessageEditor: FC<MessageEditor> = ({
|
||||
placeholder,
|
||||
options = [],
|
||||
language,
|
||||
size = 'default'
|
||||
size = 'default',
|
||||
className
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const form = Form.useFormInstance();
|
||||
@@ -78,12 +81,12 @@ const MessageEditor: FC<MessageEditor> = ({
|
||||
|
||||
if (!isArray) {
|
||||
return (
|
||||
<Space size={8} direction="vertical" className="rb:w-full rb:border rb:border-[#DFE4ED] rb:rounded-md rb:px-2 rb:py-1.5" data-editor-type={parentName === 'template' ? 'template' : undefined}>
|
||||
<Flex gap={8} vertical className={clsx("rb-border rb:rounded-lg rb:px-2! rb:py-1.5!", className)} data-editor-type={parentName === 'template' ? 'template' : undefined}>
|
||||
<Row>
|
||||
<Col span={12}>
|
||||
{typeof title === 'string'
|
||||
? <div className={clsx("rb:text-[12px] rb:font-medium rb:py-1 rb:leading-2", {
|
||||
'rb:bg-[#F6F8FC] rb:border rb:border-[#DFE4ED] rb:rounded-sm rb:px-2': titleVariant === 'outlined'
|
||||
? <div className={clsx("rb:text-[12px] rb:text-[#212332] rb:font-medium rb:leading-4", {
|
||||
'rb:bg-[#F6F6F6] rb-border rb:rounded-md rb:px-2 rb:py-1': titleVariant === 'outlined'
|
||||
})}>{title ?? t('workflow.answerDesc')}</div>
|
||||
: title}
|
||||
</Col>
|
||||
@@ -91,14 +94,14 @@ const MessageEditor: FC<MessageEditor> = ({
|
||||
<Form.Item name={parentName} noStyle>
|
||||
<Editor size={size} language={language} placeholder={placeholder} options={processedOptions} />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Form.List name={parentName}>
|
||||
{(fields, { add, remove }) => (
|
||||
<Space size={8} direction="vertical" className="rb:w-full">
|
||||
<Flex gap={8} vertical>
|
||||
{fields.map(({ key, name, ...restField }) => {
|
||||
const fieldValue = Array.isArray(parentName)
|
||||
? parentName.reduce((obj, key) => obj?.[key], values)
|
||||
@@ -107,12 +110,12 @@ const MessageEditor: FC<MessageEditor> = ({
|
||||
const currentRole = (fieldValue?.[name]?.role || 'USER').toUpperCase();
|
||||
|
||||
return (
|
||||
<Space key={key} size={12} direction="vertical" className="rb:w-full rb:border rb:border-[#DFE4ED] rb:rounded-md rb:p-2">
|
||||
<Flex key={key} gap={8} vertical className="rb-border rb:rounded-md rb:p-2!">
|
||||
<Row>
|
||||
<Col span={12}>
|
||||
<Form.Item {...restField} name={[name, 'role']} noStyle>
|
||||
{currentRole === 'SYSTEM' ? (
|
||||
<Input disabled className="rb:font-medium!" />
|
||||
<Input disabled className="rb:font-medium! rb:text-[#212332]!" />
|
||||
) : (
|
||||
<Select
|
||||
options={roleOptions}
|
||||
@@ -124,19 +127,19 @@ const MessageEditor: FC<MessageEditor> = ({
|
||||
</Col>
|
||||
{currentRole !== 'SYSTEM' && (
|
||||
<Col span={12}>
|
||||
<div className="rb:h-full rb:flex rb:justify-end rb:items-center">
|
||||
<Flex align="center" justify="end" className="rb:h-full">
|
||||
<div
|
||||
className="rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/workflow/delete_cycle.svg')]"
|
||||
onClick={() => remove(name)}
|
||||
></div>
|
||||
</div>
|
||||
</Flex>
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
<Form.Item {...restField} name={[name, 'content']} noStyle>
|
||||
<Editor size={size} language={language} placeholder={placeholder} options={processedOptions} />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
</Flex>
|
||||
);
|
||||
})}
|
||||
<Form.Item noStyle>
|
||||
@@ -144,7 +147,7 @@ const MessageEditor: FC<MessageEditor> = ({
|
||||
+ {t('workflow.addMessage')}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Space>
|
||||
</Flex>
|
||||
)}
|
||||
</Form.List>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { type FC } from "react";
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Form } from 'antd'
|
||||
|
||||
import RbSlider from '@/components/RbSlider'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import CustomSelect from '@/components/CustomSelect'
|
||||
import { getModelListUrl } from '@/api/models'
|
||||
|
||||
const ModelConfig: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const form = Form.useFormInstance()
|
||||
const model_id = Form.useWatch(['model_id'], form)
|
||||
console.log('ModelConfig', model_id)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form.Item
|
||||
name="model_id"
|
||||
label={t('workflow.config.llm.model_id')}
|
||||
className={model_id ? 'rb:mb-2!' : 'rb:mb-4!'}
|
||||
>
|
||||
<CustomSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
url={getModelListUrl}
|
||||
params={{ type: 'llm,chat', pagesize: 100, is_active: true }}
|
||||
hasAll={false}
|
||||
valueKey="id"
|
||||
labelKey="name"
|
||||
size="small"
|
||||
/>
|
||||
</Form.Item>
|
||||
{model_id && (
|
||||
<RbCard
|
||||
title={t('workflow.config.llm.parameterSettings')}
|
||||
headerClassName="rb:min-h-8! rb:mx-2!"
|
||||
bodyClassName="rb:pt-[14px]! rb:px-2! rb:pb-2!"
|
||||
className="rb-border! rb:mb-4!"
|
||||
variant="outlined"
|
||||
>
|
||||
<Form.Item
|
||||
name="temperature"
|
||||
label={t('workflow.config.llm.temperature')}
|
||||
className="rb:mb-1.5!"
|
||||
>
|
||||
<RbSlider
|
||||
min={0}
|
||||
max={2}
|
||||
step={0.1}
|
||||
isInput={true}
|
||||
size="small"
|
||||
className="rb:-mt-2!"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="max_tokens"
|
||||
label={t('workflow.config.llm.max_tokens')}
|
||||
className="rb:mb-0!"
|
||||
>
|
||||
<RbSlider
|
||||
min={256}
|
||||
max={32000}
|
||||
step={1}
|
||||
isInput={true}
|
||||
size="small"
|
||||
className="rb:-mt-2!"
|
||||
/>
|
||||
</Form.Item>
|
||||
</RbCard>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default ModelConfig;
|
||||
@@ -1,7 +1,6 @@
|
||||
import { type FC, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Button, Space } from 'antd'
|
||||
|
||||
import { Button, Flex } from 'antd'
|
||||
|
||||
import type { ParamItem, ParamEditModalRef } from './types'
|
||||
import ParamEditModal from './ParamEditModal'
|
||||
@@ -45,21 +44,21 @@ const ParamsList: FC<ParamsListProps> = ({
|
||||
{label}
|
||||
</div>
|
||||
|
||||
<Space size={10} direction="vertical" className="rb:w-full!">
|
||||
<Flex gap={10} vertical>
|
||||
<Button type="dashed" block size="middle" className="rb:text-[12px]!" onClick={handleAdd}>+ {t('workflow.config.parameter-extractor.addParams')}</Button>
|
||||
|
||||
{value?.map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="rb:cursor-pointer rb:group rb:py-2 rb:pl-2.5 rb:pr-2 rb:text-[12px] rb:flex rb:items-center rb:justify-between rb:bg-[#F6F8FC] rb:border rb:border-[#DFE4ED] rb:rounded-md"
|
||||
className="rb:cursor-pointer rb:group rb:py-2 rb:pl-2.5 rb:pr-2 rb:text-[12px] rb-border rb:rounded-md rb:relative"
|
||||
>
|
||||
<div>
|
||||
<span className="rb:font-medium">{item.name}</span>
|
||||
<span className="rb:text-[12px] rb:text-[#5B6167] rb:font-regular"> ({t(`workflow.config.parameter-extractor.${item.type}`)}) {item.required ? t('workflow.config.parameter-extractor.required') : ''}</span>
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-4.25 rb:mt-0.5">{item.desc}</div>
|
||||
</div>
|
||||
<Flex align="center" className="rb:leading-4 rb:w-full! rb:overflow-hidden rb:whitespace-nowrap rb:text-ellipsis rb:line-clamp-2" gap={2}>
|
||||
<span className="rb:font-medium rb:inline-block">{item.name}</span>
|
||||
<span className="rb:text-[12px] rb:text-[#5B6167] rb:font-regular">({t(`workflow.config.parameter-extractor.${item.type}`)}) {item.required ? t('workflow.config.parameter-extractor.required') : ''}</span>
|
||||
</Flex>
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:leading-4.25 rb:mt-1">{item.desc}</div>
|
||||
|
||||
<Space size={8}>
|
||||
<Flex gap={10} align="center" justify="end" className="rb:hidden! rb:group-hover:flex! rb:absolute rb:w-22 rb:pr-3! rb:right-0 rb:top-0 rb:bottom-0 rb:bg-[linear-gradient(90deg,rgba(255,255,255,0.5)_0%,#FFFFFF_50%)] rb:shadow-[0px_2px_4px_0px rgba(0,0,0,0.06)] rb:rounded-[0px_8px_8px_0px]">
|
||||
<div
|
||||
className="rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/edit.svg')] rb:hover:bg-[url('@/assets/images/edit_hover.svg')]"
|
||||
onClick={() => handleEdit(index)}
|
||||
@@ -68,10 +67,10 @@ const ParamsList: FC<ParamsListProps> = ({
|
||||
className="rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/delete.svg')] rb:hover:bg-[url('@/assets/images/delete_hover.svg')]"
|
||||
onClick={() => handleDelete(index)}
|
||||
></div>
|
||||
</Space>
|
||||
</Flex>
|
||||
</div>
|
||||
))}
|
||||
</Space>
|
||||
</Flex>
|
||||
|
||||
<ParamEditModal
|
||||
ref={paramEditModalRef}
|
||||
|
||||
105
web/src/views/Workflow/components/Properties/RadioGroupBtn.tsx
Normal file
105
web/src/views/Workflow/components/Properties/RadioGroupBtn.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-02 15:19:30
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-03 11:09:23
|
||||
*/
|
||||
/**
|
||||
* RadioGroupCard Component
|
||||
*
|
||||
* A radio group component that displays options as selectable cards with:
|
||||
* - Visual card-based selection interface
|
||||
* - Optional icons and descriptions
|
||||
* - Support for clear selection
|
||||
* - Block or inline layout modes
|
||||
* - Custom item rendering
|
||||
*
|
||||
* @component
|
||||
*/
|
||||
|
||||
import { type FC, type Key, type ReactNode, useEffect } from 'react';
|
||||
import { type RadioGroupProps } from 'antd';
|
||||
import clsx from 'clsx'
|
||||
|
||||
/** Radio card option interface */
|
||||
interface RadioCardOption {
|
||||
/** Option value */
|
||||
value: string | number | boolean | null | undefined | Key;
|
||||
/** Option label text */
|
||||
label: string;
|
||||
/** Optional description text */
|
||||
labelDesc?: string;
|
||||
/** Optional icon URL */
|
||||
icon?: string;
|
||||
/** Whether the option is disabled */
|
||||
disabled?: boolean;
|
||||
/** Additional properties */
|
||||
[key: string]: string | number | boolean | undefined | null | Key;
|
||||
}
|
||||
|
||||
/** Props interface for RadioGroupCard component */
|
||||
interface RadioCardProps extends Omit<RadioGroupProps, 'onChange'> {
|
||||
/** Array of radio card options */
|
||||
options: RadioCardOption[];
|
||||
/** Callback fired when value changes (for side effects) */
|
||||
onValueChange?: (value: string | null | undefined, option?: RadioCardOption) => void;
|
||||
/** Callback fired when selection changes */
|
||||
onChange?: (value: string | number | boolean | null | undefined | Key, option?: RadioCardOption) => void;
|
||||
/** Custom render function for each option */
|
||||
itemRender?: (option: RadioCardOption) => ReactNode;
|
||||
/** Whether clicking selected option clears selection */
|
||||
allowClear?: boolean;
|
||||
/** Whether to display cards in block (vertical) layout */
|
||||
block?: boolean;
|
||||
type?: 'inner';
|
||||
}
|
||||
|
||||
/** Radio group card component that displays options as selectable cards */
|
||||
const RadioGroupBtn: FC<RadioCardProps> = ({
|
||||
options,
|
||||
value,
|
||||
onValueChange,
|
||||
onChange,
|
||||
allowClear = true,
|
||||
block = false,
|
||||
type,
|
||||
}) => {
|
||||
/** Listen to value changes and trigger side effects via onValueChange callback */
|
||||
useEffect(() => {
|
||||
if (onValueChange) {
|
||||
onValueChange(value);
|
||||
}
|
||||
}, [value, onValueChange]);
|
||||
|
||||
/** Handle option selection with support for clear and disabled states */
|
||||
const handleChange = (option: RadioCardOption) => {
|
||||
// Ignore clicks on disabled options
|
||||
if (option.disabled) return
|
||||
if (onChange) {
|
||||
// Clear selection if allowClear is true and option is already selected
|
||||
if (allowClear && value === option.value) {
|
||||
onChange(null, undefined);
|
||||
} else {
|
||||
onChange(option.value, option);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={clsx(`rb:grid rb:grid-cols-${block ? 1 : options.length} rb:gap-1`)}>
|
||||
{/* Render each option as a selectable card */}
|
||||
{options.map(option => (
|
||||
<div key={String(option.value)} className={clsx("rb:border rb:w-full rb:leading-4.5 rb:px-2.5 rb:text-center rb:text-[12px] rb:font-medium rb:cursor-pointer", {
|
||||
'rb:opacity-[0.75]': option.disabled,
|
||||
'rb:rounded-lg rb:bg-[#F6F6F6] rb:border-[#F6F6F6] rb:py-1.25': !type,
|
||||
'rb:bg-white rb:rounded-md rb:border-white rb:py-px': type === 'inner',
|
||||
'rb:border-[#171719]! rb:bg-white': option.value === value,
|
||||
})} onClick={() => handleChange(option)}>
|
||||
{option.label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RadioGroupBtn;
|
||||
@@ -1,6 +1,6 @@
|
||||
import { type FC, useEffect, useState } from "react";
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Form, Select, InputNumber, Switch, Cascader, type CascaderProps } from 'antd'
|
||||
import { Form, Select, InputNumber, Switch, Cascader, type CascaderProps, Tooltip } from 'antd'
|
||||
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
||||
import { getToolMethods, getToolDetail, getTools } from '@/api/tools'
|
||||
import type { ToolType, ToolItem } from '@/views/ToolManagement/types'
|
||||
@@ -185,8 +185,12 @@ const ToolConfig: FC<{ options: Suggestion[]; }> = ({
|
||||
<div key={parameter.name}>
|
||||
<Form.Item
|
||||
name={['tool_parameters', parameter.name]}
|
||||
label={parameter.name}
|
||||
extra={parameter.type === 'boolean' ? undefined : parameter.description}
|
||||
label={<>
|
||||
{parameter.name}
|
||||
<Tooltip title={parameter.description} placement="right">
|
||||
<div className="rb:size-3 rb:ml-0.5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/question.svg')]"></div>
|
||||
</Tooltip>
|
||||
</>}
|
||||
rules={[
|
||||
{ required: parameter.required, message: t('common.pleaseEnter') }
|
||||
]}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Form, Input, Select, InputNumber, Checkbox, Tag } from 'antd';
|
||||
import { Form, Input, Select, InputNumber, Checkbox, Tag, Flex } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { Variable, VariableEditModalRef } from './types'
|
||||
@@ -112,8 +112,8 @@ const VariableEditModal = forwardRef<VariableEditModalRef, VariableEditModalProp
|
||||
label: t(`workflow.config.start.${key}`),
|
||||
}))}
|
||||
onChange={() => form.setFieldValue('default', undefined)}
|
||||
labelRender={(props) => <div className="rb:flex rb:justify-between rb:items-center">{props.label} <Tag color="blue">{variableType[props.value as keyof typeof variableType]}</Tag></div>}
|
||||
optionRender={(props) => <div className="rb:flex rb:justify-between rb:items-center">{props.label} <Tag color="blue">{variableType[props.value as keyof typeof variableType]}</Tag></div>}
|
||||
labelRender={(props) => <Flex align="center" justify="space-between">{props.label} <Tag color="blue">{variableType[props.value as keyof typeof variableType]}</Tag></Flex>}
|
||||
optionRender={(props) => <Flex align="center" justify="space-between">{props.label} <Tag color="blue">{variableType[props.value as keyof typeof variableType]}</Tag></Flex>}
|
||||
/>
|
||||
</FormItem>
|
||||
{/* 变量名称 */}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { type FC, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Node } from '@antv/x6';
|
||||
import { Space, Button, Divider, App } from 'antd'
|
||||
import { Space, Button, Divider, App, Flex } from 'antd'
|
||||
|
||||
import type { Variable, VariableEditModalRef } from './types'
|
||||
import type { NodeConfig } from '../../../types'
|
||||
import VariableEditModal from './VariableEditModal'
|
||||
@@ -64,40 +65,42 @@ const VariableList: FC<VariableListProps> = ({
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<Space size={10} direction="vertical" className="rb:w-full">
|
||||
<Flex gap={10} vertical>
|
||||
<div className="rb:leading-4.25 rb:text-[12px] rb:font-medium">
|
||||
{t(`workflow.config.${selectedNode?.data?.type}.${parentName}`)}
|
||||
</div>
|
||||
<Button type="dashed" block size="middle" className="rb:text-[12px]!" onClick={handleAddVariable}>+ {t('workflow.config.addVariable')}</Button>
|
||||
{Array.isArray(value) && value?.map((vo, index) =>
|
||||
<div
|
||||
<Flex
|
||||
key={`${vo.name}}-${index}`}
|
||||
className="rb:cursor-pointer rb:group rb:py-2 rb:pl-2.5 rb:pr-2 rb:text-[12px] rb:flex rb:items-center rb:justify-between rb:bg-[#F6F8FC] rb:border rb:border-[#DFE4ED] rb:rounded-md"
|
||||
align="center"
|
||||
justify="space-between"
|
||||
className="rb:cursor-pointer rb:group rb:py-2! rb:pl-2.5! rb:pr-2! rb:text-[12px] rb:bg-[#F6F6F6] rb-border rb:rounded-lg"
|
||||
onClick={() => handleEditVariable(index, vo)}
|
||||
>
|
||||
<span className="rb:font-medium">{vo.name}·{vo.description}</span>
|
||||
|
||||
<Space size={8}>
|
||||
{vo.required && <span className="rb:py-px rb:px-2 rb:bg-[#FBFDFF] rb:border rb:border-[#DFE4ED] rb:rounded-sm">{t('workflow.config.start.required')}</span>}
|
||||
<span className="rb:py-px rb:px-2 rb:bg-[#FBFDFF] rb:border rb:border-[#DFE4ED] rb:rounded-sm">{vo.type}</span>
|
||||
{vo.required && <span className="rb:py-px rb:px-2 rb:bg-white rb-border rb:rounded-sm">{t('workflow.config.start.required')}</span>}
|
||||
<span className="rb:py-px rb:px-2 rb:bg-white rb-border rb:rounded-sm">{vo.type}</span>
|
||||
<div
|
||||
className="rb:size-3 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/close.svg')] rb:hover:bg-[url('@/assets/images/close_hover.svg')]"
|
||||
className="rb:size-3 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/close.svg')]"
|
||||
onClick={(e) => handleDeleteVariable(index, vo, e)}
|
||||
></div>
|
||||
</Space>
|
||||
</div>
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
</Space>
|
||||
</Flex>
|
||||
<Divider size="small" />
|
||||
<Space size={10} direction="vertical" className="rb:w-full">
|
||||
<Flex gap={10} vertical>
|
||||
{config.sys?.map((vo, index) =>
|
||||
<div key={index} className="rb:py-2 rb:pl-2.5 rb:pr-2 rb:text-[12px] rb:flex rb:items-center rb:justify-between rb:bg-[#F6F8FC] rb:border rb:border-[#DFE4ED] rb:rounded-md">
|
||||
<Flex align="center" justify="space-between" key={index} className="rb:py-2! rb:pl-2.5! rb:pr-2! rb:text-[12px] rb:bg-[#F6F6F6] rb-border rb:rounded-md">
|
||||
<span className="rb:font-medium">sys.{vo.name}</span>
|
||||
<span className="rb:py-px rb:px-2 rb:bg-[#FBFDFF] rb:border rb:border-[#DFE4ED] rb:rounded-sm">{vo.type}</span>
|
||||
</div>
|
||||
<span className="rb:py-px rb:px-2 rb:bg-[#FBFDFF] rb-border rb:rounded-sm">{vo.type}</span>
|
||||
</Flex>
|
||||
)}
|
||||
</Space>
|
||||
</Flex>
|
||||
<VariableEditModal
|
||||
ref={variableModalRef}
|
||||
refresh={handleRefreshVariable}
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
*/
|
||||
import { type FC } from 'react'
|
||||
import clsx from 'clsx';
|
||||
import { Select, type SelectProps } from 'antd'
|
||||
import { Select, type SelectProps, Flex } from 'antd'
|
||||
|
||||
import type { Suggestion } from '../Editor/plugin/AutocompletePlugin'
|
||||
type LabelRender = SelectProps['labelRender'];
|
||||
|
||||
@@ -62,7 +63,7 @@ const VariableSelect: FC<VariableSelectProps> = ({
|
||||
if (filterOption) {
|
||||
return (
|
||||
<span
|
||||
className={clsx("rb:max-w-full rb:wrap-break-word rb:line-clamp-1 rb:border rb:border-[#DFE4ED] rb:rounded-md rb:bg-white rb:text-[12px] rb:inline-flex rb:items-center rb:px-1.5 rb:cursor-pointer", {
|
||||
className={clsx("rb:max-w-full rb:wrap-break-word rb:line-clamp-1 rb-border rb:rounded-md rb:bg-white rb:text-[12px] rb:inline-flex rb:items-center rb:px-1.5 rb:cursor-pointer", {
|
||||
'rb:leading-5.5!': size !== 'small',
|
||||
'rb:leading-4! rb:text-[10px]!': size === 'small'
|
||||
})}
|
||||
@@ -79,7 +80,7 @@ const VariableSelect: FC<VariableSelectProps> = ({
|
||||
<span className="rb:text-[#DFE4ED] rb:mx-0.5">/</span>
|
||||
</>
|
||||
)}
|
||||
<span className="rb:text-[#155EEF]">{filterOption.label}</span>
|
||||
<span className="rb:text-[#171719]">{filterOption.label}</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@@ -109,7 +110,7 @@ const VariableSelect: FC<VariableSelectProps> = ({
|
||||
const groupedOptions = Object.entries(groupedSuggestions).map(([_nodeId, suggestions]) => ({
|
||||
label: suggestions[0].nodeData.name,
|
||||
options: suggestions.map(s => ({
|
||||
label: <div className="rb:flex rb:items-center rb:gap-1 rb:justify-between"> { s.label } <span>{s.dataType}</span></div>,
|
||||
label: <Flex align="center" justify="space-between" gap={4}> {s.label} <span>{s.dataType}</span></Flex>,
|
||||
value: `{{${s.value}}}`
|
||||
}))
|
||||
}));
|
||||
|
||||
@@ -2,18 +2,15 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 15:39:59
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-03-02 17:06:41
|
||||
* @Last Modified time: 2026-03-05 17:48:25
|
||||
*/
|
||||
import { type FC, useEffect, useState, useMemo } from "react";
|
||||
import clsx from 'clsx'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Graph, Node } from '@antv/x6';
|
||||
import { Form, Input, Select, InputNumber, Switch, Divider, Space, Button } from 'antd'
|
||||
import { CaretDownOutlined, CaretRightOutlined } from '@ant-design/icons';
|
||||
import { Form, Input, Select, InputNumber, Switch, Flex, Space, Dropdown, type MenuProps, Button } from 'antd';
|
||||
|
||||
import type { NodeConfig, NodeProperties, ChatVariable } from '../../types'
|
||||
import Empty from '@/components/Empty';
|
||||
import emptyIcon from '@/assets/images/workflow/empty.png'
|
||||
import CustomSelect from "@/components/CustomSelect";
|
||||
import MessageEditor from './MessageEditor'
|
||||
import Knowledge from './Knowledge/Knowledge';
|
||||
@@ -33,19 +30,19 @@ import VariableList from './VariableList'
|
||||
import { useVariableList, getCurrentNodeVariables, getChildNodeVariables } from './hooks/useVariableList'
|
||||
import styles from './properties.module.css'
|
||||
import Editor, { type LexicalEditorProps } from "../Editor";
|
||||
import RbSlider from './RbSlider'
|
||||
import RbSlider from '@/components/RbSlider'
|
||||
import JinjaRender from './JinjaRender'
|
||||
import CodeExecution from './CodeExecution'
|
||||
import { nodeLibrary } from '../../constant';
|
||||
import RbCard from '@/components/RbCard/Card';
|
||||
import ModelConfig from './ModelConfig'
|
||||
|
||||
/**
|
||||
* Props for Properties component
|
||||
*/
|
||||
interface PropertiesProps {
|
||||
/** Currently selected node */
|
||||
selectedNode?: Node | null;
|
||||
/** Function to update selected node */
|
||||
setSelectedNode: (node: Node | null) => void;
|
||||
selectedNode: Node;
|
||||
/** Reference to graph instance */
|
||||
graphRef: React.MutableRefObject<Graph | undefined>;
|
||||
/** Handler for blank canvas click */
|
||||
@@ -418,13 +415,40 @@ const Properties: FC<PropertiesProps> = ({
|
||||
blankClick()
|
||||
}
|
||||
}
|
||||
const handleClick: MenuProps['onClick'] = (e) => {
|
||||
switch(e.key) {
|
||||
case 'delete':
|
||||
selectedNode.remove()
|
||||
break;
|
||||
case 'copy':
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={clsx("rb:w-75 rb:fixed rb:right-0 rb:top-16 rb:bottom-0 rb:p-3 rb:pb-6", styles.properties)}>
|
||||
<div className="rb:font-medium rb:leading-5 rb:pb-3 rb:mb-3 rb:border-b rb:border-b-[#DFE4ED]">{t('workflow.nodeProperties')}</div>
|
||||
{!selectedNode
|
||||
? <Empty url={emptyIcon} size={140} className="rb:h-full rb:mx-15" title={t('workflow.empty')} />
|
||||
: <div className="rb:h-[calc(100%-20px)] rb:overflow-x-hidden rb:overflow-y-auto">
|
||||
<div className={clsx("rb:w-90 rb:fixed rb:right-2.5 rb:top-18.5 rb:bottom-2.5 rb:z-1000", styles.properties)}>
|
||||
<RbCard
|
||||
title={t('workflow.nodeProperties')}
|
||||
extra={<Space>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{ key: 'delete', icon: <div className="rb:size-4 rb:bg-cover rb:bg-[url('src/assets/images/common/delete_dark.svg')]"></div>, label: <Flex>{t('common.delete')}</Flex>},
|
||||
// { key: 'copy', icon: <div className="rb:size-4 rb:bg-cover rb:bg-[url('@/assets/images/common/copy_dark.svg')]"></div>, label: t('common.copy') }
|
||||
],
|
||||
onClick: handleClick
|
||||
}}
|
||||
>
|
||||
<div className="rb:cursor-pointer rb:size-4 rb:hover:bg-[#F6F6F6] rb:rounded-sm rb:bg-cover rb:bg-[url(@/assets/images/common/dash.svg)]">
|
||||
</div>
|
||||
</Dropdown>
|
||||
<div className="rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/close.svg')]" onClick={blankClick}></div>
|
||||
</Space>}
|
||||
headerType="borderless"
|
||||
headerClassName={clsx("rb:font-[MiSans-Bold] rb:font-bold rb:min-h-[48px]!")}
|
||||
className="rb:h-full! rb:hover:shadow-none!"
|
||||
bodyClassName={clsx('rb:overflow-y-auto! rb:h-[calc(100vh-131px)]! rb:px-3! rb:pt-0! rb:pb-3!')}
|
||||
>
|
||||
<Form form={form} size="small" layout="vertical">
|
||||
<Form.Item name="name" label={t('workflow.nodeName')}>
|
||||
<Input
|
||||
@@ -485,7 +509,7 @@ const Properties: FC<PropertiesProps> = ({
|
||||
|
||||
if (selectedNode?.data?.type === 'start' && key === 'variables' && config.type === 'define') {
|
||||
return (
|
||||
<Form.Item key={key} name={key}>
|
||||
<Form.Item key={key} name={key} className="rb:mb-0!">
|
||||
<VariableList
|
||||
parentName={key}
|
||||
selectedNode={selectedNode}
|
||||
@@ -495,6 +519,9 @@ const Properties: FC<PropertiesProps> = ({
|
||||
)
|
||||
}
|
||||
|
||||
if (key === 'model_id' && selectedNode?.data?.type === 'llm') {
|
||||
return <ModelConfig />
|
||||
}
|
||||
if (selectedNode?.data?.type === 'llm' && key === 'messages' && config.type === 'define') {
|
||||
// 为llm节点且isArray=true时添加context变量支持
|
||||
let contextVariableList = [...getFilteredVariableList('llm')];
|
||||
@@ -548,9 +575,10 @@ const Properties: FC<PropertiesProps> = ({
|
||||
|
||||
if (config.type === 'messageEditor') {
|
||||
return (
|
||||
<Form.Item key={key} name={key} label={selectedNode?.data?.type === 'memory-write' ? t(`workflow.config.${selectedNode?.data?.type}.${key}`) : undefined }>
|
||||
<Form.Item key={key} name={key} label={selectedNode?.data?.type === 'memory-write' ? t(`workflow.config.${selectedNode?.data?.type}.${key}`) : undefined}>
|
||||
<MessageEditor
|
||||
title={t(`workflow.config.${selectedNode?.data?.type}.${key}`)}
|
||||
placeholder={t(config.placeholder || 'common.pleaseEnter')}
|
||||
isArray={!!config.isArray}
|
||||
parentName={key}
|
||||
language={config.language as LexicalEditorProps['language']}
|
||||
@@ -586,7 +614,7 @@ const Properties: FC<PropertiesProps> = ({
|
||||
}
|
||||
if (config.type === 'caseList') {
|
||||
return (
|
||||
<Form.Item key={key} name={key}>
|
||||
<Form.Item key={key} name={key} noStyle>
|
||||
<CaseList
|
||||
name={key}
|
||||
options={getFilteredVariableList(selectedNode?.data?.type, key)}
|
||||
@@ -683,7 +711,15 @@ const Properties: FC<PropertiesProps> = ({
|
||||
: t(`workflow.config.${selectedNode?.data?.type}.${key}`)
|
||||
}
|
||||
layout={config.type === 'switch' ? 'horizontal' : 'vertical'}
|
||||
className={key === 'parallel_count' ? 'rb:-mt-3! rb:leading-3.5!' : ''}
|
||||
className={
|
||||
key === 'parallel' && values?.parallel
|
||||
? 'rb:mb-1!'
|
||||
: key === 'vision' && values?.vision
|
||||
? 'rb:mb-2!'
|
||||
: key === 'group' && values?.group
|
||||
? 'rb:mb-3!'
|
||||
: ''
|
||||
}
|
||||
hidden={Boolean(config.hidden)}
|
||||
>
|
||||
{config.type === 'input'
|
||||
@@ -702,7 +738,13 @@ const Properties: FC<PropertiesProps> = ({
|
||||
onChange={(value) => form.setFieldValue(key, value)}
|
||||
/>
|
||||
: config.type === 'slider'
|
||||
? <RbSlider min={config.min} max={config.max} step={config.step} />
|
||||
? <RbSlider
|
||||
min={config.min}
|
||||
max={config.max}
|
||||
step={config.step || 0.01}
|
||||
isInput={true}
|
||||
size="small"
|
||||
/>
|
||||
: config.type === 'customSelect'
|
||||
? <CustomSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
@@ -769,7 +811,7 @@ const Properties: FC<PropertiesProps> = ({
|
||||
options={getFilteredVariableList(selectedNode?.data?.type, key)}
|
||||
/>
|
||||
: config.type === 'editor'
|
||||
? <Editor options={variableList} variant="outlined" size="small" />
|
||||
? <Editor options={variableList} variant="outlined" size="small" placeholder={config.placeholder || t('common.pleaseEnter')} />
|
||||
: null
|
||||
}
|
||||
</Form.Item>
|
||||
@@ -779,23 +821,26 @@ const Properties: FC<PropertiesProps> = ({
|
||||
</Form>
|
||||
|
||||
{currentNodeVariables.length > 0 && !(!values?.group && selectedNode.getData().type === 'var-aggregator') &&
|
||||
<div className="rb:pb-3">
|
||||
<Divider />
|
||||
<Space size={8} direction="vertical" className="rb:max-w-full!">
|
||||
<div className="rb:font-medium rb:text-[12px] rb:leading-4.5 rb:cursor-pointer rb:ml-4" onClick={handleToggle}>
|
||||
<div className="rb:text-[12px] rb:leading-4.5">
|
||||
<Flex gap={8} vertical>
|
||||
<Flex align="center" className="rb:font-medium rb:cursor-pointer" onClick={handleToggle}>
|
||||
{t('workflow.config.output')}
|
||||
{outputCollapsed ? <CaretRightOutlined /> : <CaretDownOutlined />}
|
||||
</div>
|
||||
<div
|
||||
className={clsx("rb:size-3 rb:bg-cover rb:bg-[url('src/assets/images/common/caret_right_outlined.svg')]", {
|
||||
'rb:rotate-90': !outputCollapsed
|
||||
})}
|
||||
></div>
|
||||
</Flex>
|
||||
{!outputCollapsed && currentNodeVariables.map(vo => (
|
||||
<div key={vo.value} className="rb:ml-4 rb:text-[12px] rb:flex rb:gap-2">
|
||||
<Flex key={vo.value} gap={4}>
|
||||
<span className="rb:font-medium">{vo.label}</span>
|
||||
<span className="rb:text-[#5B6167]">{vo.dataType}</span>
|
||||
</div>
|
||||
<span className="rb:text-[#212332]">{vo.dataType}</span>
|
||||
</Flex>
|
||||
))}
|
||||
</Space>
|
||||
</Flex>
|
||||
</div>
|
||||
}
|
||||
</div>}
|
||||
</RbCard>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,51 +7,46 @@
|
||||
}
|
||||
.properties :global(.ant-input-outlined.ant-input-disabled),
|
||||
.properties :global(.ant-input-outlined[disabled]) {
|
||||
background-color: #F6F8FC;
|
||||
background-color: #F6F6F6;
|
||||
}
|
||||
.properties :global(.ant-select-single.ant-select-sm){
|
||||
.properties :global(.ant-select-single.ant-select-sm) {
|
||||
height: 28px;
|
||||
}
|
||||
.properties :global(.ant-select-multiple.ant-select-sm .ant-select-selector) {
|
||||
min-height: 28px;
|
||||
}
|
||||
.properties :global(:not(.select).ant-select-single.ant-select-sm.ant-select-borderless) {
|
||||
height: 22px;
|
||||
}
|
||||
.properties :global(.select.ant-select-single.ant-select-sm.ant-select-borderless) {
|
||||
height: 28px;
|
||||
}
|
||||
.properties :global(.ant-table-wrapper .ant-table-thead>tr>th),
|
||||
.properties :global(.ant-table-wrapper .ant-table-thead>tr>td),
|
||||
.properties :global(.ant-table-wrapper .ant-table) {
|
||||
background-color: #F6F8FC;
|
||||
}
|
||||
.properties :global(.ant-table-wrapper .ant-table),
|
||||
.properties :global(.ant-table-container),
|
||||
.properties :global(.ant-table-wrapper table) {
|
||||
border-radius: 6px;
|
||||
}
|
||||
.properties :global(.ant-table-wrapper .ant-table-container table>thead>tr:first-child>*:first-child) {
|
||||
border-start-start-radius: 6px;
|
||||
}
|
||||
.properties :global(.ant-table-wrapper .ant-table-container table>thead>tr:first-child>*:last-child) {
|
||||
border-start-end-radius: 6px;
|
||||
}
|
||||
.properties :global(.ant-table-row:last-child .ant-table-cell:first-child) {
|
||||
border-bottom-left-radius: 6px;
|
||||
}
|
||||
.properties :global(.ant-table-row:last-child .ant-table-cell:last-child) {
|
||||
border-bottom-right-radius: 6px;
|
||||
}
|
||||
.properties :global(.ant-table-wrapper .ant-table) {
|
||||
background: transparent;
|
||||
}
|
||||
.properties :global(.ant-table-wrapper .ant-table-container) {
|
||||
border-start-start-radius: 6px;
|
||||
border-start-end-radius: 6px;
|
||||
}
|
||||
.properties :global(.ant-table-container) {
|
||||
/* border-left: none;
|
||||
border-top: none;
|
||||
border-bottom: none; */
|
||||
border: none;
|
||||
}
|
||||
.properties :global(.ant-table-wrapper .ant-table-tbody>tr>td),
|
||||
.properties :global(.ant-table-wrapper .ant-table-tbody>tr.ant-table-placeholder:hover>th),
|
||||
.properties :global(.ant-table-wrapper .ant-table-tbody>tr.ant-table-placeholder:hover>td),
|
||||
.properties :global(.ant-table-wrapper .ant-table-tbody>tr.ant-table-placeholder),
|
||||
.properties :global(.ant-table-wrapper .ant-table) {
|
||||
background-color: #F6F8FC;
|
||||
.properties :global(.ant-table-wrapper .ant-table-wrapper .ant-table-thead>tr>th),
|
||||
.properties :global(.ant-table-wrapper .ant-table-thead>tr>td)
|
||||
.properties :global(.ant-table-wrapper .ant-table),
|
||||
.properties :global(.ant-table-container),
|
||||
.properties :global(.ant-table-wrapper .ant-table-tbody .ant-table-row>.ant-table-cell-row-hover) {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
}
|
||||
.properties :global(.ant-table-wrapper .ant-table.ant-table-small .ant-table-title),
|
||||
.properties :global(.ant-table-wrapper .ant-table.ant-table-small .ant-table-footer),
|
||||
.properties :global(.ant-table-wrapper .ant-table.ant-table-small .ant-table-cell),
|
||||
.properties :global(.ant-table-wrapper .ant-table.ant-table-small .ant-table-thead>tr>th),
|
||||
.properties :global(.ant-table-wrapper .ant-table.ant-table-small .ant-table-tbody>tr>th),
|
||||
.properties :global(.ant-table-wrapper .ant-table.ant-table-small .ant-table-tbody>tr>td),
|
||||
.properties :global(.ant-table-wrapper .ant-table.ant-table-small tfoot>tr>th),
|
||||
.properties :global(.ant-table-wrapper .ant-table.ant-table-small tfoot>tr>td) {
|
||||
padding: 0;
|
||||
}
|
||||
.properties :global(.ant-table-wrapper .ant-table.ant-table-small .ant-table-tbody>tr>td) {
|
||||
padding: 4px 4px 0 0;
|
||||
}
|
||||
.properties :global(.ant-form-item-horizontal.ant-form-item .ant-form-item-control-input-content:has(> .ant-switch:only-child, > .ant-rate:only-child)) {
|
||||
display: flex;
|
||||
@@ -62,12 +57,15 @@
|
||||
}
|
||||
.properties :global(.ant-form-item) {
|
||||
margin-bottom: 16px;
|
||||
line-height: 17px;
|
||||
}
|
||||
.properties :global(.ant-form-item .ant-form-item-label>label) {
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
height: 17px;
|
||||
}
|
||||
.properties :global(.ant-select-single.ant-select-sm .ant-select-selector),
|
||||
.properties :global(.ant-select-multiple.ant-select-sm .ant-select-selector),
|
||||
.properties :global(.ant-select-dropdown .ant-select-item),.properties :global(.ant-input-number-sm) {
|
||||
font-size: 12px;
|
||||
}
|
||||
@@ -77,7 +75,7 @@
|
||||
.properties :global(.ant-slider-horizontal .ant-slider-step) {
|
||||
height: 6px;
|
||||
}
|
||||
.properties :global(.ant-select-single.ant-select-sm:not(.ant-select-customize-input) .ant-select-selector) {
|
||||
.properties :global(.ant-select-single.ant-select-sm:not(.ant-select-customize-input) .ant-select-selector){
|
||||
padding: 0 4px 0 6px ;
|
||||
}
|
||||
.properties :global(.ant-select-single.ant-select-sm:not(.ant-select-customize-input).ant-select-show-arrow .ant-select-selection-item),
|
||||
@@ -88,6 +86,72 @@
|
||||
font-size: 10px;
|
||||
inset-inline-end: 6px;
|
||||
}
|
||||
.properties :global(.ant-select-multiple.ant-select-sm .ant-select-selector) {
|
||||
padding-block: 2px;
|
||||
}
|
||||
.properties :global(.ant-input-sm) {
|
||||
padding: 3.6px 7px;
|
||||
}
|
||||
.properties :global(.ant-divider) {
|
||||
border-block-start: 1px solid #EBEBEB;
|
||||
}
|
||||
.properties :global(.ant-switch.ant-switch-small) {
|
||||
height: 16px;
|
||||
line-height: 16px;
|
||||
}
|
||||
.properties :global(.ant-switch.ant-switch-small .ant-switch-handle) {
|
||||
width: 13.6px;
|
||||
height: 13.6px;
|
||||
top: 1.5px;
|
||||
inset-inline-start: 1.5px;
|
||||
}
|
||||
.properties :global(.ant-switch.ant-switch-small.ant-switch-checked .ant-switch-handle) {
|
||||
inset-inline-start: calc(100% - 15px);
|
||||
}
|
||||
.properties :global(.ant-select .ant-select-selection-item) {
|
||||
font-weight: 500;
|
||||
}
|
||||
.properties :global(.ant-input-number-sm) {
|
||||
padding-top: 3px;
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
|
||||
.properties :global(.select.ant-select-borderless .ant-select-selector) {
|
||||
background: #F6F6F6 !important;
|
||||
}
|
||||
.properties :global(.ant-radio-group) {
|
||||
display: grid;
|
||||
grid-template-columns: 48px 73px 172px;
|
||||
gap: 8px 18px;
|
||||
}
|
||||
.properties :global(.ant-radio-wrapper) {
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
margin-inline-end: 0;
|
||||
}
|
||||
.properties :global(.ant-radio-wrapper .ant-radio-inner) {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-color: #A8A9AA;
|
||||
}
|
||||
.properties :global(.ant-radio-wrapper .ant-radio-inner::after) {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin-block-start: -6px;
|
||||
margin-inline-start: -6px;
|
||||
}
|
||||
.properties :global(.ant-radio-wrapper .ant-radio-checked .ant-radio-inner) {
|
||||
border-color: #171719;
|
||||
}
|
||||
.properties :global(.ant-radio-wrapper .ant-radio-checked::after) {
|
||||
border: 4px solid #171719;
|
||||
}
|
||||
.properties :global(.ant-radio-wrapper span.ant-radio+*) {
|
||||
padding-inline-start: 3px;
|
||||
padding-inline-end: 0;
|
||||
}
|
||||
.properties :global(.ant-select-multiple.ant-select-sm .ant-select-selection-overflow .ant-select-selection-item) {
|
||||
padding-inline-start: 0px;
|
||||
border-radius: 4px;
|
||||
margin-block: 0px;
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 15:06:18
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-11 12:07:20
|
||||
* @Last Modified time: 2026-03-06 14:52:02
|
||||
*/
|
||||
import LoopNode from './components/Nodes/LoopNode';
|
||||
import NormalNode from './components/Nodes/NormalNode';
|
||||
@@ -13,40 +13,24 @@ import type { PortMetadata, GroupMetadata } from '@antv/x6/lib/model/port';
|
||||
import type { ReactShapeConfig } from '@antv/x6-react-shape';
|
||||
|
||||
// Import workflow icons
|
||||
import startIcon from '@/assets/images/workflow/start.png';
|
||||
import endIcon from '@/assets/images/workflow/end.png';
|
||||
// import answerIcon from '@/assets/images/workflow/answer.png';
|
||||
import llmIcon from '@/assets/images/workflow/llm.png';
|
||||
// import modelSelectionIcon from '@/assets/images/workflow/model_selection.png';
|
||||
// import modelVotingIcon from '@/assets/images/workflow/model_voting.png';
|
||||
import ragIcon from '@/assets/images/workflow/rag.png';
|
||||
// import classificationIcon from '@/assets/images/workflow/classification.png';
|
||||
import parameterExtractionIcon from '@/assets/images/workflow/parameter_extraction.png';
|
||||
// import taskPlanningIcon from '@/assets/images/workflow/task_planning.png';
|
||||
// import reasoningControlIcon from '@/assets/images/workflow/reasoning_control.png';
|
||||
// import selfReflectionIcon from '@/assets/images/workflow/self_reflection.png';
|
||||
// import memoryEnhancementIcon from '@/assets/images/workflow/memory_enhancement.png';
|
||||
// import agentSchedulingIcon from '@/assets/images/workflow/agent_scheduling.png';
|
||||
// import agentCollaborationIcon from '@/assets/images/workflow/agent_collaboration.png';
|
||||
// import agentArbitrationIcon from '@/assets/images/workflow/agent_arbitration.png';
|
||||
import conditionIcon from '@/assets/images/workflow/condition.png';
|
||||
import iterationIcon from '@/assets/images/workflow/iteration.png';
|
||||
import loopIcon from '@/assets/images/workflow/loop.png';
|
||||
// import parallelIcon from '@/assets/images/workflow/parallel.png';
|
||||
import aggregatorIcon from '@/assets/images/workflow/aggregator.png';
|
||||
import httpRequestIcon from '@/assets/images/workflow/http_request.png';
|
||||
import toolsIcon from '@/assets/images/workflow/tools.png';
|
||||
import codeExecutionIcon from '@/assets/images/workflow/code_execution.png';
|
||||
import templateRenderingIcon from '@/assets/images/workflow/template_rendering.png';
|
||||
// import sensitiveDetectionIcon from '@/assets/images/workflow/sensitive_detection.png';
|
||||
// import outputAuditIcon from '@/assets/images/workflow/output_audit.png';
|
||||
// import selfOptimizationIcon from '@/assets/images/workflow/self_optimization.png';
|
||||
// import processEvolutionIcon from '@/assets/images/workflow/process_evolution.png';
|
||||
import questionClassifierIcon from '@/assets/images/workflow/question-classifier.png'
|
||||
import breakIcon from '@/assets/images/workflow/break.png'
|
||||
import assignerIcon from '@/assets/images/workflow/assigner.png'
|
||||
import memoryReadIcon from '@/assets/images/workflow/memory-read.png'
|
||||
import memoryWriteIcon from '@/assets/images/workflow/memory-write.png'
|
||||
import startIcon from '@/assets/images/workflow/start.svg';
|
||||
import endIcon from '@/assets/images/workflow/end.svg';
|
||||
import llmIcon from '@/assets/images/workflow/llm.svg';
|
||||
import ragIcon from '@/assets/images/workflow/rag.svg';
|
||||
import parameterExtractionIcon from '@/assets/images/workflow/parameter_extraction.svg';
|
||||
import conditionIcon from '@/assets/images/workflow/condition.svg';
|
||||
import iterationIcon from '@/assets/images/workflow/iteration.svg';
|
||||
import loopIcon from '@/assets/images/workflow/loop.svg';
|
||||
import aggregatorIcon from '@/assets/images/workflow/aggregator.svg';
|
||||
import httpRequestIcon from '@/assets/images/workflow/http_request.svg';
|
||||
import toolsIcon from '@/assets/images/workflow/tools.svg';
|
||||
import codeExecutionIcon from '@/assets/images/workflow/code_execution.svg';
|
||||
import templateRenderingIcon from '@/assets/images/workflow/template_rendering.svg';
|
||||
import questionClassifierIcon from '@/assets/images/workflow/question-classifier.svg'
|
||||
import breakIcon from '@/assets/images/workflow/break.svg'
|
||||
import assignerIcon from '@/assets/images/workflow/assigner.svg'
|
||||
import memoryReadIcon from '@/assets/images/workflow/memory-read.svg'
|
||||
import memoryWriteIcon from '@/assets/images/workflow/memory-write.svg'
|
||||
import unknownIcon from '@/assets/images/workflow/unknown.svg'
|
||||
|
||||
import { memoryConfigListUrl } from '@/api/memory'
|
||||
@@ -119,21 +103,21 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
{ type: "llm", icon: llmIcon,
|
||||
config: {
|
||||
model_id: {
|
||||
type: 'customSelect',
|
||||
type: 'define',
|
||||
url: getModelListUrl,
|
||||
params: { type: 'llm,chat', pagesize: 100, is_active: true }, // llm/chat
|
||||
valueKey: 'id',
|
||||
labelKey: 'name',
|
||||
},
|
||||
temperature: {
|
||||
type: 'slider',
|
||||
type: 'define',
|
||||
max: 2,
|
||||
min: 0,
|
||||
step: 0.1,
|
||||
defaultValue: 0.7
|
||||
},
|
||||
max_tokens: {
|
||||
type: 'slider',
|
||||
type: 'define',
|
||||
max: 32000,
|
||||
min: 256,
|
||||
step: 1,
|
||||
@@ -171,8 +155,6 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
}
|
||||
}
|
||||
},
|
||||
// { type: "model_selection", icon: modelSelectionIcon },
|
||||
// { type: "model_voting", icon: modelVotingIcon },
|
||||
{ type: "knowledge-retrieval", icon: ragIcon,
|
||||
config: {
|
||||
query: {
|
||||
@@ -183,7 +165,6 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
}
|
||||
}
|
||||
},
|
||||
// { type: "classification", icon: classificationIcon },
|
||||
{ type: "parameter-extractor", icon: parameterExtractionIcon,
|
||||
config: {
|
||||
model_id: {
|
||||
@@ -260,14 +241,6 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
},
|
||||
]
|
||||
},
|
||||
// {
|
||||
// category: "agentCollaborationNode",
|
||||
// nodes: [
|
||||
// { type: "agent_scheduling", icon: agentSchedulingIcon },
|
||||
// { type: "agent_collaboration", icon: agentCollaborationIcon },
|
||||
// { type: "agent_arbitration", icon: agentArbitrationIcon }
|
||||
// ]
|
||||
// },
|
||||
{
|
||||
category: "flowControl",
|
||||
nodes: [
|
||||
@@ -306,7 +279,8 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
user_supplement_prompt: {
|
||||
type: 'messageEditor',
|
||||
isArray: false,
|
||||
titleVariant: 'borderless'
|
||||
titleVariant: 'borderless',
|
||||
placeholder: 'common.pleaseEnter'
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -366,9 +340,8 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
},
|
||||
}
|
||||
},
|
||||
{ type: "cycle-start", icon: loopIcon },
|
||||
{ type: "cycle-start", icon: startIcon },
|
||||
{ type: "break", icon: breakIcon },
|
||||
// { type: "parallel", icon: parallelIcon },
|
||||
{ type: "var-aggregator", icon: aggregatorIcon,
|
||||
config: {
|
||||
group: {
|
||||
@@ -510,20 +483,6 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
},
|
||||
]
|
||||
},
|
||||
// {
|
||||
// category: "safetyAndCompliance",
|
||||
// nodes: [
|
||||
// { type: "sensitive_detection", icon: sensitiveDetectionIcon },
|
||||
// { type: "output_audit", icon: outputAuditIcon }
|
||||
// ]
|
||||
// },
|
||||
// {
|
||||
// category: "evolutionAndGovernance",
|
||||
// nodes: [
|
||||
// { type: "self_optimization", icon: selfOptimizationIcon },
|
||||
// { type: "process_evolution", icon: processEvolutionIcon }
|
||||
// ]
|
||||
// },
|
||||
];
|
||||
export const unknownNode = {
|
||||
type: 'unknown',
|
||||
@@ -531,6 +490,10 @@ export const unknownNode = {
|
||||
}
|
||||
|
||||
export const nodeWidth = 240;
|
||||
|
||||
export const conditionNodePortItemArgsY = 60;
|
||||
export const conditionNodeItemHeight = 26;
|
||||
export const conditionNodeHeight = 110;
|
||||
/**
|
||||
* Node registration library for X6 graph
|
||||
* Maps node shapes to their React components
|
||||
@@ -557,19 +520,19 @@ export const nodeRegisterLibrary: ReactShapeConfig[] = [
|
||||
{
|
||||
shape: 'condition-node',
|
||||
width: nodeWidth,
|
||||
height: 88,
|
||||
height: conditionNodeHeight,
|
||||
component: ConditionNode,
|
||||
},
|
||||
{
|
||||
shape: 'cycle-start',
|
||||
width: 44,
|
||||
height: 44,
|
||||
width: 36,
|
||||
height: 36,
|
||||
component: GroupStartNode,
|
||||
},
|
||||
{
|
||||
shape: 'add-node',
|
||||
width: 88,
|
||||
height: 44,
|
||||
width: 100,
|
||||
height: 28,
|
||||
component: AddNode,
|
||||
},
|
||||
];
|
||||
@@ -599,10 +562,12 @@ interface NodeConfig {
|
||||
}
|
||||
|
||||
/** Edge color for normal state */
|
||||
export const edge_color = '#155EEF';
|
||||
export const edge_color = '#D4D5D9';
|
||||
/** Edge color for selected state */
|
||||
export const edge_selected_color = '#4DA8FF'
|
||||
|
||||
export const edge_selected_color = '#171719'
|
||||
export const edge_width = 2;
|
||||
/** Port color */
|
||||
export const port_color = '#171719'
|
||||
/**
|
||||
* Unified port markup configuration
|
||||
* Defines SVG elements for port rendering
|
||||
@@ -626,9 +591,9 @@ export const portAttrs = {
|
||||
body: {
|
||||
r: 6,
|
||||
magnet: true,
|
||||
stroke: edge_color,
|
||||
strokeWidth: 2,
|
||||
fill: edge_color,
|
||||
stroke: port_color,
|
||||
strokeWidth: edge_width,
|
||||
fill: port_color,
|
||||
},
|
||||
label: {
|
||||
text: '+',
|
||||
@@ -641,36 +606,33 @@ export const portAttrs = {
|
||||
},
|
||||
}
|
||||
export const portTextAttrs = { fontSize: 12, fill: '#5B6167' }
|
||||
/**
|
||||
* Port position arguments
|
||||
*/
|
||||
export const portItemArgsY = 26;
|
||||
export const portArgs = { x: nodeWidth, y: portItemArgsY }
|
||||
|
||||
const defaultPortGroup = {
|
||||
position: { name: 'absolute' },
|
||||
markup: portMarkup,
|
||||
attrs: portAttrs
|
||||
}
|
||||
|
||||
/**
|
||||
* Unified port group configuration
|
||||
* Defines port positions and attributes for different sides
|
||||
*/
|
||||
const defaultPortGroups = {
|
||||
// top: { position: 'top', markup: portMarkup, attrs: portAttrs },
|
||||
right: { position: 'right', markup: portMarkup, attrs: portAttrs },
|
||||
// bottom: { position: 'bottom', markup: portMarkup, attrs: portAttrs },
|
||||
left: { position: 'left', markup: portMarkup, attrs: portAttrs },
|
||||
}
|
||||
export const defaultAbsolutePortGroups = {
|
||||
// top: { position: 'top', markup: portMarkup, attrs: portAttrs },
|
||||
right: { position: { name: 'absolute' }, markup: portMarkup, attrs: portAttrs },
|
||||
// bottom: { position: 'bottom', markup: portMarkup, attrs: portAttrs },
|
||||
left: { position: 'left', markup: portMarkup, attrs: portAttrs },
|
||||
right: defaultPortGroup,
|
||||
left: defaultPortGroup,
|
||||
}
|
||||
/**
|
||||
* Default port items for standard nodes
|
||||
*/
|
||||
const defaultPortItems = [
|
||||
// { group: 'top' },
|
||||
{ group: 'right' },
|
||||
// { group: 'bottom' },
|
||||
{ group: 'left' }
|
||||
export const defaultPortItems = [
|
||||
{ group: 'left', args: { x: 0, y: portItemArgsY }, },
|
||||
{ group: 'right', args: { x: nodeWidth, y: portItemArgsY }, },
|
||||
];
|
||||
/**
|
||||
* Port position arguments
|
||||
*/
|
||||
export const portArgs = { x: nodeWidth, y: 42 }
|
||||
|
||||
/**
|
||||
* Graph node library configuration
|
||||
@@ -679,125 +641,132 @@ export const portArgs = { x: nodeWidth, y: 42 }
|
||||
export const graphNodeLibrary: Record<string, NodeConfig> = {
|
||||
iteration: {
|
||||
width: nodeWidth,
|
||||
height: 120,
|
||||
height: 140,
|
||||
shape: 'iteration-node',
|
||||
ports: {
|
||||
groups: defaultPortGroups,
|
||||
groups: defaultAbsolutePortGroups,
|
||||
items: defaultPortItems,
|
||||
},
|
||||
},
|
||||
loop: {
|
||||
width: nodeWidth,
|
||||
height: 120,
|
||||
height: 140,
|
||||
shape: 'loop-node',
|
||||
ports: {
|
||||
groups: defaultPortGroups,
|
||||
groups: defaultAbsolutePortGroups,
|
||||
items: defaultPortItems,
|
||||
},
|
||||
},
|
||||
'if-else': {
|
||||
width: nodeWidth,
|
||||
height: 88,
|
||||
height: conditionNodeHeight,
|
||||
shape: 'condition-node',
|
||||
ports: {
|
||||
groups: defaultAbsolutePortGroups,
|
||||
items: [
|
||||
{ group: 'left' },
|
||||
...(['IF', 'ELSE'].map((text, index) => ({
|
||||
defaultPortItems[0],
|
||||
...(['IF', 'ELSE'].map((_, index) => ({
|
||||
group: 'right',
|
||||
id: `CASE${index}`,
|
||||
args: {
|
||||
...portArgs,
|
||||
y: 30 * index + 42,
|
||||
y: portItemArgsY * index + conditionNodePortItemArgsY,
|
||||
},
|
||||
attrs: { text: { text: text, ...portTextAttrs } }
|
||||
}))),
|
||||
],
|
||||
},
|
||||
},
|
||||
'question-classifier': {
|
||||
width: nodeWidth,
|
||||
height: 88,
|
||||
height: conditionNodeHeight,
|
||||
shape: 'condition-node',
|
||||
ports: {
|
||||
groups: defaultAbsolutePortGroups,
|
||||
items: [
|
||||
{ group: 'left' },
|
||||
...(['分类1', '分类2'].map((text, index) => ({
|
||||
defaultPortItems[0],
|
||||
...(['分类1', '分类2'].map((_text, index) => ({
|
||||
group: 'right',
|
||||
id: `CASE${index}`,
|
||||
args: {
|
||||
...portArgs,
|
||||
y: 30 * index + 42,
|
||||
y: portItemArgsY * index + conditionNodePortItemArgsY,
|
||||
},
|
||||
attrs: { text: { text: text, ...portTextAttrs } }
|
||||
}))),
|
||||
],
|
||||
},
|
||||
},
|
||||
start: {
|
||||
width: nodeWidth,
|
||||
height: 64,
|
||||
height: 76,
|
||||
shape: 'normal-node',
|
||||
ports: {
|
||||
groups: {right: { position: 'right', markup: portMarkup, attrs: portAttrs }},
|
||||
items: [{ group: 'right' }],
|
||||
groups: { right: defaultPortGroup},
|
||||
items: [defaultPortItems[1]],
|
||||
},
|
||||
},
|
||||
end: {
|
||||
width: nodeWidth,
|
||||
height: 64,
|
||||
height: 76,
|
||||
shape: 'normal-node',
|
||||
ports: {
|
||||
groups: {left: { position: 'left', markup: portMarkup, attrs: portAttrs }},
|
||||
items: [{ group: 'left' }],
|
||||
groups: { left: defaultPortGroup},
|
||||
items: [defaultPortItems[0]],
|
||||
},
|
||||
},
|
||||
'cycle-start': {
|
||||
width: 44,
|
||||
height: 44,
|
||||
width: 36,
|
||||
height: 36,
|
||||
shape: 'cycle-start',
|
||||
ports: {
|
||||
groups: {right: { position: 'right', markup: portMarkup, attrs: portAttrs }},
|
||||
items: [{ group: 'right' }],
|
||||
groups: { right: defaultPortGroup },
|
||||
items: [{ group: 'right', args: { x: 36, y: 18 } }],
|
||||
},
|
||||
},
|
||||
'add-node': {
|
||||
width: 88,
|
||||
height: 44,
|
||||
width: 100,
|
||||
height: 28,
|
||||
shape: 'add-node',
|
||||
ports: {
|
||||
groups: {left: { position: 'left', markup: portMarkup, attrs: portAttrs }},
|
||||
items: [{ group: 'left' }],
|
||||
groups: { left: defaultPortGroup },
|
||||
items: [{ group: 'left', args: { x: 0, y: 18 }}],
|
||||
},
|
||||
},
|
||||
default: {
|
||||
width: nodeWidth,
|
||||
height: 64,
|
||||
height: 76,
|
||||
shape: 'normal-node',
|
||||
ports: {
|
||||
groups: defaultPortGroups,
|
||||
groups: defaultAbsolutePortGroups,
|
||||
items: defaultPortItems,
|
||||
},
|
||||
},
|
||||
cycleStart: {
|
||||
width: 44,
|
||||
height: 44,
|
||||
width: 36,
|
||||
height: 36,
|
||||
shape: 'cycle-start',
|
||||
ports: {
|
||||
groups: {right: { position: 'right', markup: portMarkup, attrs: portAttrs }},
|
||||
items: [{ group: 'right' }],
|
||||
groups: { right: defaultPortGroup },
|
||||
items: [{ group: 'right', args: { x: 36, y: 18 }}],
|
||||
},
|
||||
},
|
||||
addStart: {
|
||||
width: 88,
|
||||
height: 44,
|
||||
width: 100,
|
||||
height: 28,
|
||||
shape: 'add-node',
|
||||
ports: {
|
||||
groups: {left: { position: 'left', markup: portMarkup, attrs: portAttrs }},
|
||||
items: [{ group: 'left' }],
|
||||
groups: { left: defaultPortGroup },
|
||||
items: [{ group: 'left', args: { x: 0, y: 14 } }],
|
||||
},
|
||||
},
|
||||
break: {
|
||||
width: nodeWidth,
|
||||
height: 76,
|
||||
shape: 'normal-node',
|
||||
ports: {
|
||||
groups: { left: defaultPortGroup },
|
||||
items: [defaultPortItems[0]],
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -926,7 +895,7 @@ export const edgeAttrs = {
|
||||
attrs: {
|
||||
line: {
|
||||
stroke: edge_color,
|
||||
strokeWidth: 1,
|
||||
strokeWidth: edge_width,
|
||||
targetMarker: {
|
||||
name: 'block',
|
||||
width: 4,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 15:17:48
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-28 17:59:34
|
||||
* @Last Modified time: 2026-03-06 14:49:17
|
||||
*/
|
||||
import { useRef, useEffect, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
@@ -12,7 +12,7 @@ import { Graph, Node, MiniMap, Snapline, Clipboard, Keyboard, type Edge } from '
|
||||
import { register } from '@antv/x6-react-shape';
|
||||
import type { PortMetadata } from '@antv/x6/lib/model/port';
|
||||
|
||||
import { nodeRegisterLibrary, graphNodeLibrary, nodeLibrary, portMarkup, portAttrs, edgeAttrs, edge_color, edge_selected_color, portTextAttrs, defaultAbsolutePortGroups, nodeWidth, unknownNode } from '../constant';
|
||||
import { nodeRegisterLibrary, graphNodeLibrary, nodeLibrary, portMarkup, portAttrs, edgeAttrs, edge_color, edge_selected_color, portTextAttrs, defaultAbsolutePortGroups, nodeWidth, unknownNode, defaultPortItems, portItemArgsY, edge_width, conditionNodePortItemArgsY, conditionNodeItemHeight, conditionNodeHeight } from '../constant';
|
||||
import type { WorkflowConfig, NodeProperties, ChatVariable } from '../types';
|
||||
import { getWorkflowConfig, saveWorkflowConfig } from '@/api/application'
|
||||
|
||||
@@ -130,8 +130,8 @@ export const useWorkflowGraph = ({
|
||||
const { id, type, name, position, config = {} } = node
|
||||
let nodeLibraryConfig = [...nodeLibrary, { nodes: [unknownNode] }]
|
||||
.flatMap(category => category.nodes)
|
||||
.find(n => n.type === type)
|
||||
nodeLibraryConfig = JSON.parse(JSON.stringify({ config: {}, ...nodeLibraryConfig })) as NodeProperties
|
||||
.find(n => n.type === type) as NodeProperties
|
||||
nodeLibraryConfig = JSON.parse(JSON.stringify({ ...nodeLibraryConfig, config: nodeLibraryConfig.config || {} }))
|
||||
|
||||
if (nodeLibraryConfig?.config) {
|
||||
Object.keys(nodeLibraryConfig.config).forEach(key => {
|
||||
@@ -201,11 +201,10 @@ 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 baseHeight = 88;
|
||||
const newHeight = baseHeight + (totalPorts - 2) * 30;
|
||||
const newHeight = conditionNodeHeight + (totalPorts - 2) * conditionNodeItemHeight;
|
||||
|
||||
const portItems: PortMetadata[] = [
|
||||
{ group: 'left' },
|
||||
defaultPortItems[0],
|
||||
];
|
||||
// Add IF/ELIF/ELSE ports
|
||||
for (let i = 0; i < totalPorts; i++) {
|
||||
@@ -214,9 +213,8 @@ export const useWorkflowGraph = ({
|
||||
id: `CASE${i + 1}`,
|
||||
args: {
|
||||
x: nodeWidth,
|
||||
y: 30 * i + 42,
|
||||
y: portItemArgsY * i + conditionNodePortItemArgsY,
|
||||
},
|
||||
attrs: { text: { text: i === 0 ? 'IF' : i === totalPorts - 1 ? 'ELSE' : 'ELIF', ...portTextAttrs } }
|
||||
});
|
||||
}
|
||||
|
||||
@@ -231,11 +229,10 @@ export const useWorkflowGraph = ({
|
||||
// Generate ports dynamically for question-classifier node based on categories
|
||||
if (type === 'question-classifier' && config.categories && Array.isArray(config.categories)) {
|
||||
const categoryCount = config.categories.length;
|
||||
const baseHeight = 88;
|
||||
const newHeight = baseHeight + (categoryCount - 2) * 30;
|
||||
const newHeight = conditionNodeHeight + (categoryCount - 2) * conditionNodeItemHeight;
|
||||
|
||||
const portItems: PortMetadata[] = [
|
||||
{ group: 'left' }
|
||||
defaultPortItems[0]
|
||||
];
|
||||
|
||||
// Add category ports
|
||||
@@ -245,9 +242,8 @@ export const useWorkflowGraph = ({
|
||||
id: `CASE${index + 1}`,
|
||||
args: {
|
||||
x: nodeWidth,
|
||||
y: 30 * index + 42,
|
||||
y: portItemArgsY * index + conditionNodePortItemArgsY,
|
||||
},
|
||||
attrs: { text: { text: `分类${index + 1}`, ...portTextAttrs }}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -260,16 +256,22 @@ export const useWorkflowGraph = ({
|
||||
}
|
||||
|
||||
// Check error_handle.method config for http-request node
|
||||
if (type === 'http-request' && (config as any).error_handle?.method === 'branch') {
|
||||
if (type === 'http-request' && (nodeConfig as any).error_handle?.method === 'branch') {
|
||||
nodeConfig.ports = {
|
||||
groups: {
|
||||
right: { position: 'right', markup: portMarkup, attrs: portAttrs },
|
||||
left: { position: 'left', markup: portMarkup, attrs: portAttrs },
|
||||
},
|
||||
items: [
|
||||
{ group: 'left' },
|
||||
{ group: 'right', id: 'right' },
|
||||
{ group: 'right', id: 'ERROR', attrs: { text: { text: t('workflow.config.http-request.errorBranch'), ...portTextAttrs }}}
|
||||
defaultPortItems[0],
|
||||
{ ...defaultPortItems[1], id: 'right' },
|
||||
{
|
||||
...defaultPortItems[1],
|
||||
args: {
|
||||
x: nodeWidth,
|
||||
y: portItemArgsY + portItemArgsY,
|
||||
},
|
||||
id: 'ERROR', attrs: { text: { text: t('workflow.config.http-request.errorBranch'), ...portTextAttrs }}}
|
||||
]
|
||||
};
|
||||
}
|
||||
@@ -326,6 +328,14 @@ export const useWorkflowGraph = ({
|
||||
console.log('newWidth', newHeight, newWidth)
|
||||
|
||||
parentNode.prop('size', { width: newWidth, height: newHeight })
|
||||
|
||||
// Update x position of right group ports
|
||||
const ports = (parentNode as Node).getPorts()
|
||||
ports.forEach(port => {
|
||||
if (port.group === 'right' && port.args) {
|
||||
(parentNode as Node).portProp(port.id!, 'args/x', newWidth)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -482,6 +492,7 @@ export const useWorkflowGraph = ({
|
||||
isSelected: true,
|
||||
});
|
||||
setSelectedNode(node);
|
||||
clearEdgeSelect()
|
||||
};
|
||||
/**
|
||||
* Handle edge click event
|
||||
@@ -514,7 +525,7 @@ export const useWorkflowGraph = ({
|
||||
const clearEdgeSelect = () => {
|
||||
graphRef.current?.getEdges().forEach(e => {
|
||||
e.setAttrByPath('line/stroke', edge_color);
|
||||
e.setAttrByPath('line/strokeWidth', 1);
|
||||
e.setAttrByPath('line/strokeWidth', edge_width);
|
||||
});
|
||||
};
|
||||
/**
|
||||
@@ -524,6 +535,7 @@ export const useWorkflowGraph = ({
|
||||
clearNodeSelect();
|
||||
clearEdgeSelect();
|
||||
graphRef.current?.cleanSelection();
|
||||
setSelectedNode(null);
|
||||
};
|
||||
/**
|
||||
* Handle canvas scale/zoom event
|
||||
@@ -675,6 +687,28 @@ export const useWorkflowGraph = ({
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const nodePortClickEvent = ({ e, node, port }: { e: MouseEvent, node: Node, port: string }) => {
|
||||
e.stopPropagation();
|
||||
const portElement = e.target as HTMLElement;
|
||||
const rect = portElement.getBoundingClientRect();
|
||||
|
||||
// Create temporary popover trigger element
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.style.position = 'fixed';
|
||||
tempDiv.style.left = rect.left + 'px';
|
||||
tempDiv.style.top = rect.top + 'px';
|
||||
tempDiv.style.width = '1px';
|
||||
tempDiv.style.height = '1px';
|
||||
tempDiv.style.zIndex = '9999';
|
||||
document.body.appendChild(tempDiv);
|
||||
|
||||
// Trigger custom event to show node selection popover
|
||||
const customEvent = new CustomEvent('port:click', {
|
||||
detail: { node, port, element: tempDiv, rect }
|
||||
});
|
||||
window.dispatchEvent(customEvent);
|
||||
clearNodeSelect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle window resize event
|
||||
@@ -808,7 +842,7 @@ export const useWorkflowGraph = ({
|
||||
graphRef.current.on('edge:mouseleave', ({ edge }: { edge: Edge }) => {
|
||||
if (edge.getAttrByPath('line/stroke') !== edge_selected_color) {
|
||||
edge.setAttrByPath('line/stroke', edge_color);
|
||||
edge.setAttrByPath('line/strokeWidth', 1);
|
||||
edge.setAttrByPath('line/strokeWidth', edge_width);
|
||||
}
|
||||
});
|
||||
// Listen to node selection event
|
||||
@@ -816,33 +850,14 @@ export const useWorkflowGraph = ({
|
||||
// Listen to edge selection event
|
||||
graphRef.current.on('edge:click', edgeClick);
|
||||
// Listen to port click event
|
||||
graphRef.current.on('node:port:click', ({ e, node, port }: { e: MouseEvent, node: Node, port: string }) => {
|
||||
e.stopPropagation();
|
||||
const portElement = e.target as HTMLElement;
|
||||
const rect = portElement.getBoundingClientRect();
|
||||
|
||||
// Create temporary popover trigger element
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.style.position = 'fixed';
|
||||
tempDiv.style.left = rect.left + 'px';
|
||||
tempDiv.style.top = rect.top + 'px';
|
||||
tempDiv.style.width = '1px';
|
||||
tempDiv.style.height = '1px';
|
||||
tempDiv.style.zIndex = '9999';
|
||||
document.body.appendChild(tempDiv);
|
||||
|
||||
// Trigger custom event to show node selection popover
|
||||
const customEvent = new CustomEvent('port:click', {
|
||||
detail: { node, port, element: tempDiv, rect }
|
||||
});
|
||||
window.dispatchEvent(customEvent);
|
||||
});
|
||||
graphRef.current.on('node:port:click', nodePortClickEvent);
|
||||
// Listen to canvas click event, cancel selection
|
||||
graphRef.current.on('blank:click', blankClick);
|
||||
// Listen to zoom event
|
||||
graphRef.current.on('scale', scaleEvent);
|
||||
// Listen to node move event
|
||||
graphRef.current.on('node:moved', nodeMoved);
|
||||
graphRef.current.on('node:removed', blankClick)
|
||||
// Listen to copy keyboard event
|
||||
graphRef.current.bindKey(['ctrl+c', 'cmd+c'], copyEvent);
|
||||
// Listen to paste keyboard event
|
||||
|
||||
@@ -9,7 +9,6 @@ import { useWorkflowGraph } from './hooks/useWorkflowGraph';
|
||||
import type { WorkflowRef } from '@/views/ApplicationConfig/types'
|
||||
import Chat from './components/Chat/Chat';
|
||||
import type { ChatRef, AddChatVariableRef } from './types'
|
||||
import arrowIcon from '@/assets/images/workflow/arrow.png'
|
||||
import AddChatVariable from './components/AddChatVariable';
|
||||
|
||||
const Workflow = forwardRef<WorkflowRef>((_props, ref) => {
|
||||
@@ -23,14 +22,9 @@ const Workflow = forwardRef<WorkflowRef>((_props, ref) => {
|
||||
config,
|
||||
graphRef,
|
||||
selectedNode,
|
||||
setSelectedNode,
|
||||
zoomLevel,
|
||||
canUndo,
|
||||
canRedo,
|
||||
isHandMode,
|
||||
setIsHandMode,
|
||||
onUndo,
|
||||
onRedo,
|
||||
onDrop,
|
||||
blankClick,
|
||||
deleteEvent,
|
||||
@@ -64,22 +58,11 @@ const Workflow = forwardRef<WorkflowRef>((_props, ref) => {
|
||||
return (
|
||||
<div className="rb:h-[calc(100vh-64px)] rb:relative">
|
||||
{/* 左侧节点面板 */}
|
||||
{!collapsed && <NodeLibrary />}
|
||||
<img
|
||||
src={arrowIcon}
|
||||
className={clsx('rb:cursor-pointer rb:w-5 rb:h-10 rb:absolute rb:top-[50%] rb:z-100', {
|
||||
'rb:left-0 rb:rotate-180': collapsed,
|
||||
'rb:left-80': !collapsed
|
||||
})}
|
||||
onClick={handleToggle}
|
||||
/>
|
||||
<NodeLibrary collapsed={collapsed} handleToggle={handleToggle} />
|
||||
|
||||
{/* 右侧画布区域 */}
|
||||
<div
|
||||
className={clsx(`rb:fixed rb:top-16 rb:bottom-0 rb:right-75 rb:flex-1 rb:border-x rb:border-[#DFE4ED] rb:transition-all`, {
|
||||
'rb:left-80': !collapsed,
|
||||
'rb:left-0': collapsed
|
||||
})}
|
||||
className={clsx(`rb:fixed rb:top-18.5 rb:bottom-2.5 rb:left-0 rb:right-0 rb:transition-all`)}
|
||||
onDrop={onDrop}
|
||||
onDragOver={onDragOver}
|
||||
>
|
||||
@@ -91,17 +74,13 @@ const Workflow = forwardRef<WorkflowRef>((_props, ref) => {
|
||||
isHandMode={isHandMode}
|
||||
setIsHandMode={setIsHandMode}
|
||||
zoomLevel={zoomLevel}
|
||||
canUndo={canUndo}
|
||||
canRedo={canRedo}
|
||||
onUndo={onUndo}
|
||||
onRedo={onRedo}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 右侧属性面板 */}
|
||||
{selectedNode &&
|
||||
<Properties
|
||||
selectedNode={selectedNode}
|
||||
setSelectedNode={setSelectedNode}
|
||||
graphRef={graphRef}
|
||||
blankClick={blankClick}
|
||||
deleteEvent={deleteEvent}
|
||||
@@ -110,6 +89,7 @@ const Workflow = forwardRef<WorkflowRef>((_props, ref) => {
|
||||
config={config}
|
||||
chatVariables={chatVariables}
|
||||
/>
|
||||
}
|
||||
<Chat
|
||||
ref={chatRef}
|
||||
graphRef={graphRef}
|
||||
|
||||
@@ -10,33 +10,33 @@ export default {
|
||||
extend: {
|
||||
colors: {
|
||||
gray: {
|
||||
50: '#F7F7F7',
|
||||
100: '#F5F7FC',
|
||||
50: '#FAFAFA',
|
||||
100: '#F7F7F7',
|
||||
200: '#EBEBEB',
|
||||
300: '#d0d5dd',
|
||||
350: '#939AB1',
|
||||
400: '#A8A9AA',
|
||||
500: '#667085',
|
||||
700: '#475467',
|
||||
600: '#5B6167',
|
||||
800: '#212332',
|
||||
900: '#101828',
|
||||
900: '#171719',
|
||||
},
|
||||
primary: {
|
||||
600: '#155eef',
|
||||
600: '#171719', // default
|
||||
},
|
||||
red:{
|
||||
500: '#FF5D34'
|
||||
},
|
||||
blue: {
|
||||
500: '#E1EFFE',
|
||||
500: '#155EEF', // default
|
||||
},
|
||||
green: {
|
||||
50: '#F3FAF7',
|
||||
100: '#DEF7EC',
|
||||
600: '#369F21',
|
||||
600: '#369F21', // default
|
||||
800: '#03543F',
|
||||
},
|
||||
|
||||
},
|
||||
boxShadow: {
|
||||
'xs': '0px 4px 6px 0px rgba(0, 0, 0, 0.06)',
|
||||
|
||||
Reference in New Issue
Block a user