mirror of
https://github.com/csunny/DB-GPT.git
synced 2025-10-11 03:33:28 +00:00
153 lines
5.2 KiB
TypeScript
153 lines
5.2 KiB
TypeScript
import { IFlowNode } from '@/types/flow';
|
|
import Image from 'next/image';
|
|
import NodeParamHandler from './node-param-handler';
|
|
import classNames from 'classnames';
|
|
import { useState } from 'react';
|
|
import NodeHandler from './node-handler';
|
|
import { Popover, Tooltip } from 'antd';
|
|
import { CopyOutlined, DeleteOutlined, InfoCircleOutlined } from '@ant-design/icons';
|
|
import { useReactFlow } from 'reactflow';
|
|
import IconWrapper from '../common/icon-wrapper';
|
|
import { getUniqueNodeId } from '@/utils/flow';
|
|
import { cloneDeep } from 'lodash';
|
|
|
|
type CanvasNodeProps = {
|
|
data: IFlowNode;
|
|
};
|
|
|
|
const ICON_PATH_PREFIX = '/icons/node/';
|
|
|
|
function TypeLabel({ label }: { label: string }) {
|
|
return <div className="w-full h-8 bg-stone-100 dark:bg-zinc-700 px-2 flex items-center justify-center">{label}</div>;
|
|
}
|
|
|
|
const CanvasNode: React.FC<CanvasNodeProps> = ({ data }) => {
|
|
const node = data;
|
|
const { inputs, outputs, parameters, flow_type: flowType } = node;
|
|
const [isHovered, setIsHovered] = useState(false);
|
|
const reactFlow = useReactFlow();
|
|
|
|
function onHover() {
|
|
setIsHovered(true);
|
|
}
|
|
|
|
function onLeave() {
|
|
setIsHovered(false);
|
|
}
|
|
|
|
function copyNode(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
const nodes = reactFlow.getNodes();
|
|
const originalNode = nodes.find((item) => item.id === node.id);
|
|
if (originalNode) {
|
|
const newNodeId = getUniqueNodeId(originalNode as IFlowNode, nodes);
|
|
const cloneNode = cloneDeep(originalNode);
|
|
const duplicatedNode = {
|
|
...cloneNode,
|
|
id: newNodeId,
|
|
position: {
|
|
x: cloneNode.position.x + 400,
|
|
y: cloneNode.position.y,
|
|
},
|
|
positionAbsolute: {
|
|
x: cloneNode.positionAbsolute!.x + 400,
|
|
y: cloneNode.positionAbsolute!.y,
|
|
},
|
|
data: {
|
|
...cloneNode.data,
|
|
id: newNodeId,
|
|
},
|
|
selected: false,
|
|
};
|
|
reactFlow.setNodes((nodes) => [...nodes, duplicatedNode]);
|
|
}
|
|
}
|
|
|
|
function deleteNode(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
reactFlow.setNodes((nodes) => nodes.filter((item) => item.id !== node.id));
|
|
reactFlow.setEdges((edges) => edges.filter((edge) => edge.source !== node.id && edge.target !== node.id));
|
|
}
|
|
|
|
function renderOutput(data: IFlowNode) {
|
|
if (flowType === 'operator' && outputs?.length > 0) {
|
|
return (
|
|
<>
|
|
<TypeLabel label="Outputs" />
|
|
{(outputs || []).map((output, index) => (
|
|
<NodeHandler key={`${data.id}_input_${index}`} node={data} data={output} type="source" label="outputs" index={index} />
|
|
))}
|
|
</>
|
|
);
|
|
} else if (flowType === 'resource') {
|
|
// resource nodes show output default
|
|
return (
|
|
<>
|
|
<TypeLabel label="Outputs" />
|
|
<NodeHandler key={`${data.id}_input_0`} node={data} data={data} type="source" label="outputs" index={0} />
|
|
</>
|
|
);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Popover
|
|
placement="rightTop"
|
|
trigger={['hover']}
|
|
content={
|
|
<>
|
|
<IconWrapper className="hover:text-blue-500">
|
|
<CopyOutlined className="h-full text-lg cursor-pointer" onClick={copyNode} />
|
|
</IconWrapper>
|
|
<IconWrapper className="mt-2 hover:text-red-500">
|
|
<DeleteOutlined className="h-full text-lg cursor-pointer" onClick={deleteNode} />
|
|
</IconWrapper>
|
|
<IconWrapper className="mt-2">
|
|
<Tooltip title={<><p className="font-bold">{node.label}</p><p>{node.description}</p></>} placement="right">
|
|
<InfoCircleOutlined className="h-full text-lg cursor-pointer" />
|
|
</Tooltip>
|
|
</IconWrapper>
|
|
</>
|
|
}
|
|
>
|
|
<div
|
|
className={classNames('w-72 h-auto rounded-xl shadow-md p-0 border bg-white dark:bg-zinc-800 cursor-grab', {
|
|
'border-blue-500': node.selected || isHovered,
|
|
'border-stone-400 dark:border-white': !node.selected && !isHovered,
|
|
'border-dashed': flowType !== 'operator',
|
|
'border-red-600': node.invalid,
|
|
})}
|
|
onMouseEnter={onHover}
|
|
onMouseLeave={onLeave}
|
|
>
|
|
{/* icon and label */}
|
|
<div className="flex flex-row items-center p-2">
|
|
<Image src={'/icons/node/vis.png'} width={24} height={24} alt="" />
|
|
<p className="ml-2 text-lg font-bold text-ellipsis overflow-hidden whitespace-nowrap">{node.label}</p>
|
|
</div>
|
|
{inputs && inputs.length > 0 && (
|
|
<>
|
|
<TypeLabel label="Inputs" />
|
|
{(inputs || []).map((input, index) => (
|
|
<NodeHandler key={`${node.id}_input_${index}`} node={node} data={input} type="target" label="inputs" index={index} />
|
|
))}
|
|
</>
|
|
)}
|
|
{parameters && parameters.length > 0 && (
|
|
<>
|
|
<TypeLabel label="Parameters" />
|
|
{(parameters || []).map((parameter, index) => (
|
|
<NodeParamHandler key={`${node.id}_param_${index}`} node={node} data={parameter} label="parameters" index={index} />
|
|
))}
|
|
</>
|
|
)}
|
|
{renderOutput(node)}
|
|
</div>
|
|
</Popover>
|
|
);
|
|
};
|
|
|
|
export default CanvasNode;
|