Feat: Add the iteration Node #4242 (#4247)

### What problem does this PR solve?

Feat: Add the iteration Node #4242

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu
2024-12-27 11:24:17 +08:00
committed by GitHub
parent a6f4153775
commit a1a825c830
72 changed files with 1330 additions and 560 deletions

View File

@ -90,6 +90,7 @@ export function ButtonEdge({
// everything inside EdgeLabelRenderer has no pointer events by default
// if you have an interactive element, set pointer-events: all
pointerEvents: 'all',
zIndex: 1001, // https://github.com/xyflow/xyflow/discussions/3498
}}
className="nodrag nopan"
>

View File

@ -1,4 +1,10 @@
.canvasWrapper {
position: relative;
height: 100%;
:global(.react-flow__node-group) {
.commonNode();
padding: 0;
border: 0;
background-color: transparent;
}
}

View File

@ -4,32 +4,24 @@ import {
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip';
import { useSetModalState } from '@/hooks/common-hooks';
import { get } from 'lodash';
import { FolderInput, FolderOutput } from 'lucide-react';
import { useCallback, useEffect } from 'react';
import ReactFlow, {
Background,
ConnectionMode,
ControlButton,
Controls,
NodeMouseHandler,
} from 'reactflow';
import 'reactflow/dist/style.css';
import ChatDrawer from '../chat/drawer';
import { Operator } from '../constant';
import FormDrawer from '../flow-drawer';
import {
useGetBeginNodeDataQuery,
useHandleDrop,
useHandleExportOrImportJsonFile,
useSelectCanvasData,
useShowFormDrawer,
useShowSingleDebugDrawer,
useValidateConnection,
useWatchNodeFormDataChange,
} from '../hooks';
import { BeginQuery } from '../interface';
import { useHandleExportOrImportJsonFile } from '../hooks/use-export-json';
import { useShowDrawer } from '../hooks/use-show-drawer';
import JsonUploadModal from '../json-upload-modal';
import RunDrawer from '../run-drawer';
import { ButtonEdge } from './edge';
@ -40,6 +32,7 @@ import { CategorizeNode } from './node/categorize-node';
import { EmailNode } from './node/email-node';
import { GenerateNode } from './node/generate-node';
import { InvokeNode } from './node/invoke-node';
import { IterationNode, IterationStartNode } from './node/iteration-node';
import { KeywordNode } from './node/keyword-node';
import { LogicNode } from './node/logic-node';
import { MessageNode } from './node/message-node';
@ -66,6 +59,8 @@ const nodeTypes = {
invokeNode: InvokeNode,
templateNode: TemplateNode,
emailNode: EmailNode,
group: IterationNode,
iterationStartNode: IterationStartNode,
};
const edgeTypes = {
@ -87,66 +82,11 @@ function FlowCanvas({ drawerVisible, hideDrawer }: IProps) {
onSelectionChange,
} = useSelectCanvasData();
const isValidConnection = useValidateConnection();
const {
visible: runVisible,
showModal: showRunModal,
hideModal: hideRunModal,
} = useSetModalState();
const {
visible: chatVisible,
showModal: showChatModal,
hideModal: hideChatModal,
} = useSetModalState();
const {
singleDebugDrawerVisible,
showSingleDebugDrawer,
hideSingleDebugDrawer,
} = useShowSingleDebugDrawer();
const controlIconClassname = 'text-black';
const { formDrawerVisible, hideFormDrawer, showFormDrawer, clickedNode } =
useShowFormDrawer();
const onPaneClick = useCallback(() => {
hideFormDrawer();
}, [hideFormDrawer]);
const { onDrop, onDragOver, setReactFlowInstance } = useHandleDrop();
useWatchNodeFormDataChange();
const hideRunOrChatDrawer = useCallback(() => {
hideChatModal();
hideRunModal();
hideDrawer();
}, [hideChatModal, hideDrawer, hideRunModal]);
const onNodeClick: NodeMouseHandler = useCallback(
(e, node) => {
if (node.data.label !== Operator.Note) {
hideSingleDebugDrawer();
hideRunOrChatDrawer();
showFormDrawer(node);
}
// handle single debug icon click
if (
get(e.target, 'dataset.play') === 'true' ||
get(e.target, 'parentNode.dataset.play') === 'true'
) {
showSingleDebugDrawer();
}
},
[
hideRunOrChatDrawer,
hideSingleDebugDrawer,
showFormDrawer,
showSingleDebugDrawer,
],
);
const getBeginNodeDataQuery = useGetBeginNodeDataQuery();
const {
handleExportJson,
handleImportJson,
@ -155,25 +95,25 @@ function FlowCanvas({ drawerVisible, hideDrawer }: IProps) {
hideFileUploadModal,
} = useHandleExportOrImportJsonFile();
useEffect(() => {
if (drawerVisible) {
const query: BeginQuery[] = getBeginNodeDataQuery();
if (query.length > 0) {
showRunModal();
hideChatModal();
} else {
showChatModal();
hideRunModal();
}
}
}, [
hideChatModal,
hideRunModal,
const {
onNodeClick,
onPaneClick,
clickedNode,
formDrawerVisible,
hideFormDrawer,
singleDebugDrawerVisible,
hideSingleDebugDrawer,
showSingleDebugDrawer,
chatVisible,
runVisible,
hideRunOrChatDrawer,
showChatModal,
showRunModal,
} = useShowDrawer({
drawerVisible,
getBeginNodeDataQuery,
]);
hideDrawer,
});
useWatchNodeFormDataChange();
return (
<div className={styles.canvasWrapper}>
@ -222,6 +162,7 @@ function FlowCanvas({ drawerVisible, hideDrawer }: IProps) {
strokeWidth: 2,
stroke: 'rgb(202 197 245)',
},
zIndex: 1001, // https://github.com/xyflow/xyflow/discussions/3498
}}
deleteKeyCode={['Delete', 'Backspace']}
>

View File

@ -44,7 +44,9 @@ export function BeginNode({ selected, data }: NodeProps<NodeData>) {
fontSize={24}
color={operatorMap[data.label as Operator].color}
></OperatorIcon>
<div className={styles.nodeTitle}>{t(`flow.begin`)}</div>
<div className="truncate text-center font-semibold text-sm">
{t(`flow.begin`)}
</div>
</Flex>
<Flex gap={8} vertical className={styles.generateParameters}>
{query.map((x, idx) => {

View File

@ -3,6 +3,7 @@ import { CopyOutlined } from '@ant-design/icons';
import { Flex, MenuProps } from 'antd';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { Operator } from '../../constant';
import { useDuplicateNode } from '../../hooks';
import useGraphStore from '../../store';
@ -15,10 +16,17 @@ interface IProps {
const NodeDropdown = ({ id, iconFontColor, label }: IProps) => {
const { t } = useTranslation();
const deleteNodeById = useGraphStore((store) => store.deleteNodeById);
const deleteIterationNodeById = useGraphStore(
(store) => store.deleteIterationNodeById,
);
const deleteNode = useCallback(() => {
deleteNodeById(id);
}, [id, deleteNodeById]);
if (label === Operator.Iteration) {
deleteIterationNodeById(id);
} else {
deleteNodeById(id);
}
}, [label, deleteIterationNodeById, id, deleteNodeById]);
const duplicateNode = useDuplicateNode();

View File

@ -4,7 +4,7 @@ import { Flex } from 'antd';
import classNames from 'classnames';
import { get } from 'lodash';
import { Handle, NodeProps, Position } from 'reactflow';
import { useGetComponentLabelByValue } from '../../hooks';
import { useGetComponentLabelByValue } from '../../hooks/use-get-begin-query';
import { IGenerateParameter, NodeData } from '../../interface';
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
import styles from './index.less';

View File

@ -1,15 +1,3 @@
.commonNode() {
box-shadow:
-6px 0 12px 0 rgba(179, 177, 177, 0.08),
-3px 0 6px -4px rgba(0, 0, 0, 0.12),
-6px 0 16px 6px rgba(0, 0, 0, 0.05);
padding: 10px;
border-radius: 10px;
background: white;
width: 200px;
}
.dark {
background: rgb(63, 63, 63) !important;
}
@ -43,6 +31,22 @@
border: 1.5px solid rgb(59, 118, 244);
}
.selectedIterationNode {
border-bottom: 1.5px solid rgb(59, 118, 244);
border-left: 1.5px solid rgb(59, 118, 244);
border-right: 1.5px solid rgb(59, 118, 244);
}
.iterationHeader {
.commonNodeShadow();
}
.selectedHeader {
border-top: 1.9px solid rgb(59, 118, 244);
border-left: 1.9px solid rgb(59, 118, 244);
border-right: 1.9px solid rgb(59, 118, 244);
}
.handle {
display: inline-flex;
align-items: center;
@ -133,6 +137,12 @@
}
}
.iterationNode {
.commonNodeShadow();
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
}
.nodeText {
padding-inline: 0.4em;
padding-block: 0.2em 0.1em;
@ -142,12 +152,6 @@
.textEllipsis();
}
.nodeTitle {
font-weight: 600;
text-align: center;
.textEllipsis();
}
.nodeHeader {
padding-bottom: 12px;
}

View File

@ -0,0 +1,118 @@
import { useTheme } from '@/components/theme-provider';
import { cn } from '@/lib/utils';
import { ListRestart } from 'lucide-react';
import { Handle, NodeProps, NodeResizeControl, Position } from 'reactflow';
import { NodeData } from '../../interface';
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
import styles from './index.less';
import NodeHeader from './node-header';
function ResizeIcon() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
strokeWidth="2"
stroke="#5025f9"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
style={{ position: 'absolute', right: 5, bottom: 5 }}
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<polyline points="16 20 20 20 20 16" />
<line x1="14" y1="14" x2="20" y2="20" />
<polyline points="8 4 4 4 4 8" />
<line x1="4" y1="4" x2="10" y2="10" />
</svg>
);
}
const controlStyle = {
background: 'transparent',
border: 'none',
};
export function IterationNode({
id,
data,
isConnectable = true,
selected,
}: NodeProps<NodeData>) {
const { theme } = useTheme();
return (
<section
className={cn(
'w-full h-full bg-zinc-200 opacity-70',
styles.iterationNode,
{
['bg-gray-800']: theme === 'dark',
[styles.selectedIterationNode]: selected,
},
)}
>
<NodeResizeControl style={controlStyle} minWidth={100} minHeight={50}>
<ResizeIcon />
</NodeResizeControl>
<Handle
id="c"
type="source"
position={Position.Left}
isConnectable={isConnectable}
className={styles.handle}
style={LeftHandleStyle}
></Handle>
<Handle
type="source"
position={Position.Right}
isConnectable={isConnectable}
className={styles.handle}
id="b"
style={RightHandleStyle}
></Handle>
<NodeHeader
id={id}
name={data.name}
label={data.label}
wrapperClassName={cn(
'p-2 bg-white rounded-t-[10px] absolute w-full top-[-60px] left-[-0.3px]',
styles.iterationHeader,
{
[`${styles.dark} text-white`]: theme === 'dark',
[styles.selectedHeader]: selected,
},
)}
></NodeHeader>
</section>
);
}
export function IterationStartNode({
isConnectable = true,
selected,
}: NodeProps<NodeData>) {
const { theme } = useTheme();
return (
<section
className={cn('bg-white p-2 rounded-xl', {
[styles.dark]: theme === 'dark',
[styles.selectedNode]: selected,
})}
>
<Handle
type="source"
position={Position.Right}
isConnectable={isConnectable}
className={styles.handle}
style={RightHandleStyle}
></Handle>
<div>
<ListRestart className="size-7" />
</div>
</section>
);
}

View File

@ -8,15 +8,17 @@ import NodeDropdown from './dropdown';
import { NextNodePopover } from './popover';
import { RunTooltip } from '../../flow-tooltip';
import styles from './index.less';
interface IProps {
id: string;
label: string;
name: string;
gap?: number;
className?: string;
wrapperClassName?: string;
}
const ExcludedRunStateOperators = [Operator.Answer];
export function RunStatus({ id, name, label }: IProps) {
const { t } = useTranslate('flow');
return (
@ -35,10 +37,17 @@ export function RunStatus({ id, name, label }: IProps) {
);
}
const NodeHeader = ({ label, id, name, gap = 4, className }: IProps) => {
const NodeHeader = ({
label,
id,
name,
gap = 4,
className,
wrapperClassName,
}: IProps) => {
return (
<section>
{label !== Operator.Answer && (
<section className={wrapperClassName}>
{!ExcludedRunStateOperators.includes(label as Operator) && (
<RunStatus id={id} name={name} label={label}></RunStatus>
)}
<Flex
@ -52,7 +61,9 @@ const NodeHeader = ({ label, id, name, gap = 4, className }: IProps) => {
name={label as Operator}
color={operatorMap[label as Operator].color}
></OperatorIcon>
<span className={styles.nodeTitle}>{name}</span>
<span className="truncate text-center font-semibold text-sm">
{name}
</span>
<NodeDropdown id={id} label={label}></NodeDropdown>
</Flex>
</section>

View File

@ -3,7 +3,7 @@ import get from 'lodash/get';
import React, { MouseEventHandler, useCallback, useMemo } from 'react';
import JsonView from 'react18-json-view';
import 'react18-json-view/src/style.css';
import { useGetComponentLabelByValue, useReplaceIdWithText } from '../../hooks';
import { useReplaceIdWithText } from '../../hooks';
import { useTheme } from '@/components/theme-provider';
import {
@ -20,6 +20,7 @@ import {
TableRow,
} from '@/components/ui/table';
import { useTranslate } from '@/hooks/common-hooks';
import { useGetComponentLabelByValue } from '../../hooks/use-get-begin-query';
interface IProps extends React.PropsWithChildren {
nodeId: string;

View File

@ -2,7 +2,7 @@ import { useTheme } from '@/components/theme-provider';
import { Divider, Flex } from 'antd';
import classNames from 'classnames';
import { Handle, NodeProps, Position } from 'reactflow';
import { useGetComponentLabelByValue } from '../../hooks';
import { useGetComponentLabelByValue } from '../../hooks/use-get-begin-query';
import { ISwitchCondition, NodeData } from '../../interface';
import { RightHandleStyle } from './handle-icon';
import { useBuildSwitchHandlePositions } from './hooks';

View File

@ -1,13 +1,13 @@
import { useTheme } from '@/components/theme-provider';
import { Flex } from 'antd';
import classNames from 'classnames';
import { get } from 'lodash';
import { Handle, NodeProps, Position } from 'reactflow';
import { useGetComponentLabelByValue } from '../../hooks';
import { useGetComponentLabelByValue } from '../../hooks/use-get-begin-query';
import { IGenerateParameter, NodeData } from '../../interface';
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
import NodeHeader from './node-header';
import { useTheme } from '@/components/theme-provider';
import styles from './index.less';
export function TemplateNode({