mirror of
https://github.com/infiniflow/ragflow.git
synced 2025-12-30 00:32:30 +08:00
### 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:
@ -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"
|
||||
>
|
||||
|
||||
@ -1,4 +1,10 @@
|
||||
.canvasWrapper {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
:global(.react-flow__node-group) {
|
||||
.commonNode();
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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']}
|
||||
>
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
118
web/src/pages/flow/canvas/node/iteration-node.tsx
Normal file
118
web/src/pages/flow/canvas/node/iteration-node.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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({
|
||||
|
||||
Reference in New Issue
Block a user