diff --git a/web/src/pages/data-flow/canvas/context-menu/index.less b/web/src/pages/data-flow/canvas/context-menu/index.less
new file mode 100644
index 000000000..5594aa912
--- /dev/null
+++ b/web/src/pages/data-flow/canvas/context-menu/index.less
@@ -0,0 +1,18 @@
+.contextMenu {
+ background: rgba(255, 255, 255, 0.1);
+ border-style: solid;
+ box-shadow: 10px 19px 20px rgba(0, 0, 0, 10%);
+ position: absolute;
+ z-index: 10;
+ button {
+ border: none;
+ display: block;
+ padding: 0.5em;
+ text-align: left;
+ width: 100%;
+ }
+
+ button:hover {
+ background: rgba(255, 255, 255, 0.1);
+ }
+}
diff --git a/web/src/pages/data-flow/canvas/context-menu/index.tsx b/web/src/pages/data-flow/canvas/context-menu/index.tsx
new file mode 100644
index 000000000..6cb306af9
--- /dev/null
+++ b/web/src/pages/data-flow/canvas/context-menu/index.tsx
@@ -0,0 +1,107 @@
+import { NodeMouseHandler, useReactFlow } from '@xyflow/react';
+import { useCallback, useRef, useState } from 'react';
+
+import styles from './index.less';
+
+export interface INodeContextMenu {
+ id: string;
+ top: number;
+ left: number;
+ right?: number;
+ bottom?: number;
+ [key: string]: unknown;
+}
+
+export function NodeContextMenu({
+ id,
+ top,
+ left,
+ right,
+ bottom,
+ ...props
+}: INodeContextMenu) {
+ const { getNode, setNodes, addNodes, setEdges } = useReactFlow();
+
+ const duplicateNode = useCallback(() => {
+ const node = getNode(id);
+ const position = {
+ x: node?.position?.x || 0 + 50,
+ y: node?.position?.y || 0 + 50,
+ };
+
+ addNodes({
+ ...(node || {}),
+ data: node?.data,
+ selected: false,
+ dragging: false,
+ id: `${node?.id}-copy`,
+ position,
+ });
+ }, [id, getNode, addNodes]);
+
+ const deleteNode = useCallback(() => {
+ setNodes((nodes) => nodes.filter((node) => node.id !== id));
+ setEdges((edges) => edges.filter((edge) => edge.source !== id));
+ }, [id, setNodes, setEdges]);
+
+ return (
+
+
+ node: {id}
+
+
+
+
+ );
+}
+
+/* @deprecated
+ */
+export const useHandleNodeContextMenu = (sideWidth: number) => {
+ const [menu, setMenu] = useState({} as INodeContextMenu);
+ const ref = useRef(null);
+
+ const onNodeContextMenu: NodeMouseHandler = useCallback(
+ (event, node) => {
+ // Prevent native context menu from showing
+ event.preventDefault();
+
+ // Calculate position of the context menu. We want to make sure it
+ // doesn't get positioned off-screen.
+ const pane = ref.current?.getBoundingClientRect();
+ // setMenu({
+ // id: node.id,
+ // top: event.clientY < pane.height - 200 ? event.clientY : 0,
+ // left: event.clientX < pane.width - 200 ? event.clientX : 0,
+ // right: event.clientX >= pane.width - 200 ? pane.width - event.clientX : 0,
+ // bottom:
+ // event.clientY >= pane.height - 200 ? pane.height - event.clientY : 0,
+ // });
+
+ setMenu({
+ id: node.id,
+ top: event.clientY - 144,
+ left: event.clientX - sideWidth,
+ // top: event.clientY < pane.height - 200 ? event.clientY - 72 : 0,
+ // left: event.clientX < pane.width - 200 ? event.clientX : 0,
+ });
+ },
+ [sideWidth],
+ );
+
+ // Close the context menu if it's open whenever the window is clicked.
+ const onPaneClick = useCallback(
+ () => setMenu({} as INodeContextMenu),
+ [setMenu],
+ );
+
+ return { onNodeContextMenu, menu, onPaneClick, ref };
+};
diff --git a/web/src/pages/data-flow/canvas/context.tsx b/web/src/pages/data-flow/canvas/context.tsx
new file mode 100644
index 000000000..203c37e99
--- /dev/null
+++ b/web/src/pages/data-flow/canvas/context.tsx
@@ -0,0 +1,56 @@
+import {
+ createContext,
+ ReactNode,
+ useCallback,
+ useContext,
+ useRef,
+} from 'react';
+
+interface DropdownContextType {
+ canShowDropdown: () => boolean;
+ setActiveDropdown: (type: 'handle' | 'drag') => void;
+ clearActiveDropdown: () => void;
+}
+
+const DropdownContext = createContext(null);
+
+export const useDropdownManager = () => {
+ const context = useContext(DropdownContext);
+ if (!context) {
+ throw new Error('useDropdownManager must be used within DropdownProvider');
+ }
+ return context;
+};
+
+interface DropdownProviderProps {
+ children: ReactNode;
+}
+
+export const DropdownProvider = ({ children }: DropdownProviderProps) => {
+ const activeDropdownRef = useRef<'handle' | 'drag' | null>(null);
+
+ const canShowDropdown = useCallback(() => {
+ const current = activeDropdownRef.current;
+ return !current;
+ }, []);
+
+ const setActiveDropdown = useCallback((type: 'handle' | 'drag') => {
+ activeDropdownRef.current = type;
+ }, []);
+
+ const clearActiveDropdown = useCallback(() => {
+ activeDropdownRef.current = null;
+ }, []);
+
+ const value: DropdownContextType = {
+ canShowDropdown,
+ setActiveDropdown,
+ clearActiveDropdown,
+ };
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/web/src/pages/data-flow/canvas/edge/index.tsx b/web/src/pages/data-flow/canvas/edge/index.tsx
new file mode 100644
index 000000000..3e1b57e85
--- /dev/null
+++ b/web/src/pages/data-flow/canvas/edge/index.tsx
@@ -0,0 +1,126 @@
+import {
+ BaseEdge,
+ Edge,
+ EdgeLabelRenderer,
+ EdgeProps,
+ getBezierPath,
+} from '@xyflow/react';
+import { memo } from 'react';
+import useGraphStore from '../../store';
+
+import { useFetchAgent } from '@/hooks/use-agent-request';
+import { cn } from '@/lib/utils';
+import { useMemo } from 'react';
+import { NodeHandleId, Operator } from '../../constant';
+
+function InnerButtonEdge({
+ id,
+ sourceX,
+ sourceY,
+ targetX,
+ targetY,
+ sourcePosition,
+ targetPosition,
+ source,
+ target,
+ style = {},
+ markerEnd,
+ selected,
+ data,
+ sourceHandleId,
+}: EdgeProps>) {
+ const deleteEdgeById = useGraphStore((state) => state.deleteEdgeById);
+ const [edgePath, labelX, labelY] = getBezierPath({
+ sourceX,
+ sourceY,
+ sourcePosition,
+ targetX,
+ targetY,
+ targetPosition,
+ });
+ const selectedStyle = useMemo(() => {
+ return selected ? { strokeWidth: 1, stroke: 'rgba(76, 164, 231, 1)' } : {};
+ }, [selected]);
+
+ const onEdgeClick = () => {
+ deleteEdgeById(id);
+ };
+
+ // highlight the nodes that the workflow passes through
+ const { data: flowDetail } = useFetchAgent();
+
+ const graphPath = useMemo(() => {
+ // TODO: this will be called multiple times
+ const path = flowDetail?.dsl?.path ?? [];
+ // The second to last
+ const previousGraphPath: string[] = path.at(-2) ?? [];
+ let graphPath: string[] = path.at(-1) ?? [];
+ // The last of the second to last article
+ const previousLatestElement = previousGraphPath.at(-1);
+ if (previousGraphPath.length > 0 && previousLatestElement) {
+ graphPath = [previousLatestElement, ...graphPath];
+ }
+ return Array.isArray(graphPath) ? graphPath : [];
+ }, [flowDetail.dsl?.path]);
+
+ const highlightStyle = useMemo(() => {
+ const idx = graphPath.findIndex((x) => x === source);
+ if (idx !== -1) {
+ // The set of elements following source
+ const slicedGraphPath = graphPath.slice(idx + 1);
+ if (slicedGraphPath.some((x) => x === target)) {
+ return { strokeWidth: 1, stroke: 'red' };
+ }
+ }
+ return {};
+ }, [source, target, graphPath]);
+
+ const visible = useMemo(() => {
+ return (
+ data?.isHovered &&
+ sourceHandleId !== NodeHandleId.Tool &&
+ sourceHandleId !== NodeHandleId.AgentBottom && // The connection between the agent node and the tool node does not need to display the delete button
+ !target.startsWith(Operator.Tool)
+ );
+ }, [data?.isHovered, sourceHandleId, target]);
+
+ return (
+ <>
+
+
+
+
+
+
+
+ >
+ );
+}
+
+export const ButtonEdge = memo(InnerButtonEdge);
diff --git a/web/src/pages/data-flow/canvas/index.less b/web/src/pages/data-flow/canvas/index.less
new file mode 100644
index 000000000..0183d41b5
--- /dev/null
+++ b/web/src/pages/data-flow/canvas/index.less
@@ -0,0 +1,11 @@
+.canvasWrapper {
+ position: relative;
+ height: calc(100% - 64px);
+ :global(.react-flow__node-group) {
+ .commonNode();
+ border-radius: 0 0 10px 10px;
+ padding: 0;
+ border: 0;
+ background-color: transparent;
+ }
+}
diff --git a/web/src/pages/data-flow/canvas/index.tsx b/web/src/pages/data-flow/canvas/index.tsx
new file mode 100644
index 000000000..eb91ee584
--- /dev/null
+++ b/web/src/pages/data-flow/canvas/index.tsx
@@ -0,0 +1,331 @@
+import { useIsDarkTheme, useTheme } from '@/components/theme-provider';
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
+} from '@/components/ui/tooltip';
+import { useSetModalState } from '@/hooks/common-hooks';
+import { cn } from '@/lib/utils';
+import {
+ Connection,
+ ConnectionMode,
+ ControlButton,
+ Controls,
+ NodeTypes,
+ Position,
+ ReactFlow,
+} from '@xyflow/react';
+import '@xyflow/react/dist/style.css';
+import { NotebookPen } from 'lucide-react';
+import { useCallback, useRef, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { AgentBackground } from '../components/background';
+import { AgentInstanceContext, HandleContext } from '../context';
+
+import FormSheet from '../form-sheet/next';
+import {
+ useHandleDrop,
+ useSelectCanvasData,
+ useValidateConnection,
+} from '../hooks';
+import { useAddNode } from '../hooks/use-add-node';
+import { useBeforeDelete } from '../hooks/use-before-delete';
+import { useMoveNote } from '../hooks/use-move-note';
+import { useDropdownManager } from './context';
+
+import {
+ useHideFormSheetOnNodeDeletion,
+ useShowDrawer,
+} from '../hooks/use-show-drawer';
+import RunSheet from '../run-sheet';
+import { ButtonEdge } from './edge';
+import styles from './index.less';
+import { RagNode } from './node';
+import { AgentNode } from './node/agent-node';
+import { BeginNode } from './node/begin-node';
+import { CategorizeNode } from './node/categorize-node';
+import { InnerNextStepDropdown } from './node/dropdown/next-step-dropdown';
+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';
+import NoteNode from './node/note-node';
+import { RelevantNode } from './node/relevant-node';
+import { RetrievalNode } from './node/retrieval-node';
+import { RewriteNode } from './node/rewrite-node';
+import { SwitchNode } from './node/switch-node';
+import { TemplateNode } from './node/template-node';
+import { ToolNode } from './node/tool-node';
+
+export const nodeTypes: NodeTypes = {
+ ragNode: RagNode,
+ categorizeNode: CategorizeNode,
+ beginNode: BeginNode,
+ relevantNode: RelevantNode,
+ logicNode: LogicNode,
+ noteNode: NoteNode,
+ switchNode: SwitchNode,
+ generateNode: GenerateNode,
+ retrievalNode: RetrievalNode,
+ messageNode: MessageNode,
+ rewriteNode: RewriteNode,
+ keywordNode: KeywordNode,
+ invokeNode: InvokeNode,
+ templateNode: TemplateNode,
+ // emailNode: EmailNode,
+ group: IterationNode,
+ iterationStartNode: IterationStartNode,
+ agentNode: AgentNode,
+ toolNode: ToolNode,
+};
+
+const edgeTypes = {
+ buttonEdge: ButtonEdge,
+};
+
+interface IProps {
+ drawerVisible: boolean;
+ hideDrawer(): void;
+}
+
+function AgentCanvas({ drawerVisible, hideDrawer }: IProps) {
+ const { t } = useTranslation();
+ const {
+ nodes,
+ edges,
+ onConnect: originalOnConnect,
+ onEdgesChange,
+ onNodesChange,
+ onSelectionChange,
+ onEdgeMouseEnter,
+ onEdgeMouseLeave,
+ } = useSelectCanvasData();
+ const isValidConnection = useValidateConnection();
+
+ const { onDrop, onDragOver, setReactFlowInstance, reactFlowInstance } =
+ useHandleDrop();
+
+ const {
+ onNodeClick,
+ clickedNode,
+ formDrawerVisible,
+ hideFormDrawer,
+ singleDebugDrawerVisible,
+ hideSingleDebugDrawer,
+ showSingleDebugDrawer,
+ chatVisible,
+ runVisible,
+ hideRunOrChatDrawer,
+ showChatModal,
+ showFormDrawer,
+ } = useShowDrawer({
+ drawerVisible,
+ hideDrawer,
+ });
+
+ const { handleBeforeDelete } = useBeforeDelete();
+
+ const { addCanvasNode, addNoteNode } = useAddNode(reactFlowInstance);
+
+ const { ref, showImage, hideImage, imgVisible, mouse } = useMoveNote();
+
+ const { theme } = useTheme();
+
+ const isDarkTheme = useIsDarkTheme();
+
+ useHideFormSheetOnNodeDeletion({ hideFormDrawer });
+
+ const { visible, hideModal, showModal } = useSetModalState();
+ const [dropdownPosition, setDropdownPosition] = useState({ x: 0, y: 0 });
+
+ const isConnectedRef = useRef(false);
+ const connectionStartRef = useRef<{
+ nodeId: string;
+ handleId: string;
+ } | null>(null);
+
+ const preventCloseRef = useRef(false);
+
+ const { setActiveDropdown, clearActiveDropdown } = useDropdownManager();
+
+ const onPaneClick = useCallback(() => {
+ hideFormDrawer();
+ if (visible && !preventCloseRef.current) {
+ hideModal();
+ clearActiveDropdown();
+ }
+ if (imgVisible) {
+ addNoteNode(mouse);
+ hideImage();
+ }
+ }, [
+ hideFormDrawer,
+ visible,
+ hideModal,
+ imgVisible,
+ addNoteNode,
+ mouse,
+ hideImage,
+ clearActiveDropdown,
+ ]);
+
+ const onConnect = (connection: Connection) => {
+ originalOnConnect(connection);
+ isConnectedRef.current = true;
+ };
+
+ const OnConnectStart = (event: any, params: any) => {
+ isConnectedRef.current = false;
+
+ if (params && params.nodeId && params.handleId) {
+ connectionStartRef.current = {
+ nodeId: params.nodeId,
+ handleId: params.handleId,
+ };
+ } else {
+ connectionStartRef.current = null;
+ }
+ };
+
+ const OnConnectEnd = (event: MouseEvent | TouchEvent) => {
+ if ('clientX' in event && 'clientY' in event) {
+ const { clientX, clientY } = event;
+ setDropdownPosition({ x: clientX, y: clientY });
+ if (!isConnectedRef.current) {
+ setActiveDropdown('drag');
+ showModal();
+ preventCloseRef.current = true;
+ setTimeout(() => {
+ preventCloseRef.current = false;
+ }, 300);
+ }
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ {t('flow.note')}
+
+
+
+
+ {visible && (
+
+ {
+ hideModal();
+ clearActiveDropdown();
+ }}
+ position={dropdownPosition}
+ >
+
+
+
+ )}
+
+
+ {formDrawerVisible && (
+
+
+
+ )}
+ {runVisible && (
+
+ )}
+
+ );
+}
+
+export default AgentCanvas;
diff --git a/web/src/pages/data-flow/canvas/node/agent-node.tsx b/web/src/pages/data-flow/canvas/node/agent-node.tsx
new file mode 100644
index 000000000..42b489a41
--- /dev/null
+++ b/web/src/pages/data-flow/canvas/node/agent-node.tsx
@@ -0,0 +1,116 @@
+import LLMLabel from '@/components/llm-select/llm-label';
+import { IAgentNode } from '@/interfaces/database/flow';
+import { Handle, NodeProps, Position } from '@xyflow/react';
+import { get } from 'lodash';
+import { memo, useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
+import { AgentExceptionMethod, NodeHandleId } from '../../constant';
+import useGraphStore from '../../store';
+import { isBottomSubAgent } from '../../utils';
+import { CommonHandle } from './handle';
+import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
+import styles from './index.less';
+import NodeHeader from './node-header';
+import { NodeWrapper } from './node-wrapper';
+import { ToolBar } from './toolbar';
+
+function InnerAgentNode({
+ id,
+ data,
+ isConnectable = true,
+ selected,
+}: NodeProps) {
+ const edges = useGraphStore((state) => state.edges);
+ const { t } = useTranslation();
+
+ const isHeadAgent = useMemo(() => {
+ return !isBottomSubAgent(edges, id);
+ }, [edges, id]);
+
+ const exceptionMethod = useMemo(() => {
+ return get(data, 'form.exception_method');
+ }, [data]);
+
+ const isGotoMethod = useMemo(() => {
+ return exceptionMethod === AgentExceptionMethod.Goto;
+ }, [exceptionMethod]);
+
+ return (
+
+
+ {isHeadAgent && (
+ <>
+
+
+ >
+ )}
+
+
+
+
+
+
+
+
+
+ {(isGotoMethod ||
+ exceptionMethod === AgentExceptionMethod.Comment) && (
+
+ {t('flow.onFailure')}
+
+ {t(`flow.${exceptionMethod}`)}
+
+
+ )}
+
+ {isGotoMethod && (
+
+ )}
+
+
+ );
+}
+
+export const AgentNode = memo(InnerAgentNode);
diff --git a/web/src/pages/data-flow/canvas/node/begin-node.tsx b/web/src/pages/data-flow/canvas/node/begin-node.tsx
new file mode 100644
index 000000000..be80d56ae
--- /dev/null
+++ b/web/src/pages/data-flow/canvas/node/begin-node.tsx
@@ -0,0 +1,62 @@
+import { IBeginNode } from '@/interfaces/database/flow';
+import { cn } from '@/lib/utils';
+import { NodeProps, Position } from '@xyflow/react';
+import get from 'lodash/get';
+import { memo } from 'react';
+import { useTranslation } from 'react-i18next';
+import {
+ BeginQueryType,
+ BeginQueryTypeIconMap,
+ NodeHandleId,
+ Operator,
+} from '../../constant';
+import { BeginQuery } from '../../interface';
+import OperatorIcon from '../../operator-icon';
+import { CommonHandle } from './handle';
+import { RightHandleStyle } from './handle-icon';
+import styles from './index.less';
+import { NodeWrapper } from './node-wrapper';
+
+// TODO: do not allow other nodes to connect to this node
+function InnerBeginNode({ data, id, selected }: NodeProps) {
+ const { t } = useTranslation();
+ const inputs: Record = get(data, 'form.inputs', {});
+
+ return (
+
+
+
+
+
+
+ {t(`flow.begin`)}
+
+
+
+ {Object.entries(inputs).map(([key, val], idx) => {
+ const Icon = BeginQueryTypeIconMap[val.type as BeginQueryType];
+ return (
+
+
+
+ {val.name}
+ {val.optional ? 'Yes' : 'No'}
+
+ );
+ })}
+
+
+ );
+}
+
+export const BeginNode = memo(InnerBeginNode);
diff --git a/web/src/pages/data-flow/canvas/node/card.tsx b/web/src/pages/data-flow/canvas/node/card.tsx
new file mode 100644
index 000000000..042ca45e0
--- /dev/null
+++ b/web/src/pages/data-flow/canvas/node/card.tsx
@@ -0,0 +1,57 @@
+import { Button } from '@/components/ui/button';
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardFooter,
+ CardHeader,
+ CardTitle,
+} from '@/components/ui/card';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@/components/ui/select';
+
+export function CardWithForm() {
+ return (
+
+
+ Create project
+ Deploy your new project in one-click.
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/web/src/pages/data-flow/canvas/node/categorize-node.tsx b/web/src/pages/data-flow/canvas/node/categorize-node.tsx
new file mode 100644
index 000000000..a54136b5a
--- /dev/null
+++ b/web/src/pages/data-flow/canvas/node/categorize-node.tsx
@@ -0,0 +1,62 @@
+import LLMLabel from '@/components/llm-select/llm-label';
+import { ICategorizeNode } from '@/interfaces/database/flow';
+import { NodeProps, Position } from '@xyflow/react';
+import { get } from 'lodash';
+import { memo } from 'react';
+import { NodeHandleId } from '../../constant';
+import { CommonHandle } from './handle';
+import { RightHandleStyle } from './handle-icon';
+import NodeHeader from './node-header';
+import { NodeWrapper } from './node-wrapper';
+import { ToolBar } from './toolbar';
+import { useBuildCategorizeHandlePositions } from './use-build-categorize-handle-positions';
+
+export function InnerCategorizeNode({
+ id,
+ data,
+ selected,
+}: NodeProps) {
+ const { positions } = useBuildCategorizeHandlePositions({ data, id });
+ return (
+
+
+
+
+
+
+
+
+
+
+ {positions.map((position) => {
+ return (
+
+
+ {position.name}
+
+
+
+ );
+ })}
+
+
+
+ );
+}
+
+export const CategorizeNode = memo(InnerCategorizeNode);
diff --git a/web/src/pages/data-flow/canvas/node/dropdown.tsx b/web/src/pages/data-flow/canvas/node/dropdown.tsx
new file mode 100644
index 000000000..dd5263abc
--- /dev/null
+++ b/web/src/pages/data-flow/canvas/node/dropdown.tsx
@@ -0,0 +1,58 @@
+import OperateDropdown from '@/components/operate-dropdown';
+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';
+
+interface IProps {
+ id: string;
+ iconFontColor?: string;
+ label: string;
+}
+
+const NodeDropdown = ({ id, iconFontColor, label }: IProps) => {
+ const { t } = useTranslation();
+ const deleteNodeById = useGraphStore((store) => store.deleteNodeById);
+ const deleteIterationNodeById = useGraphStore(
+ (store) => store.deleteIterationNodeById,
+ );
+
+ const deleteNode = useCallback(() => {
+ if (label === Operator.Iteration) {
+ deleteIterationNodeById(id);
+ } else {
+ deleteNodeById(id);
+ }
+ }, [label, deleteIterationNodeById, id, deleteNodeById]);
+
+ const duplicateNode = useDuplicateNode();
+
+ const items: MenuProps['items'] = [
+ {
+ key: '2',
+ onClick: () => duplicateNode(id, label),
+ label: (
+
+ {t('common.copy')}
+
+
+ ),
+ },
+ ];
+
+ return (
+
+ );
+};
+
+export default NodeDropdown;
diff --git a/web/src/pages/data-flow/canvas/node/dropdown/next-step-dropdown.tsx b/web/src/pages/data-flow/canvas/node/dropdown/next-step-dropdown.tsx
new file mode 100644
index 000000000..6d9f32453
--- /dev/null
+++ b/web/src/pages/data-flow/canvas/node/dropdown/next-step-dropdown.tsx
@@ -0,0 +1,295 @@
+import {
+ Accordion,
+ AccordionContent,
+ AccordionItem,
+ AccordionTrigger,
+} from '@/components/ui/accordion';
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuTrigger,
+} from '@/components/ui/dropdown-menu';
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
+} from '@/components/ui/tooltip';
+import { IModalProps } from '@/interfaces/common';
+import { Operator } from '@/pages/agent/constant';
+import { AgentInstanceContext, HandleContext } from '@/pages/agent/context';
+import OperatorIcon from '@/pages/agent/operator-icon';
+import { Position } from '@xyflow/react';
+import { t } from 'i18next';
+import { lowerFirst } from 'lodash';
+import {
+ PropsWithChildren,
+ createContext,
+ memo,
+ useContext,
+ useEffect,
+ useRef,
+} from 'react';
+import { useTranslation } from 'react-i18next';
+
+type OperatorItemProps = {
+ operators: Operator[];
+ isCustomDropdown?: boolean;
+ mousePosition?: { x: number; y: number };
+};
+
+const HideModalContext = createContext['showModal']>(() => {});
+const OnNodeCreatedContext = createContext<
+ ((newNodeId: string) => void) | undefined
+>(undefined);
+
+function OperatorItemList({
+ operators,
+ isCustomDropdown = false,
+ mousePosition,
+}: OperatorItemProps) {
+ const { addCanvasNode } = useContext(AgentInstanceContext);
+ const handleContext = useContext(HandleContext);
+ const hideModal = useContext(HideModalContext);
+ const onNodeCreated = useContext(OnNodeCreatedContext);
+ const { t } = useTranslation();
+
+ const handleClick = (operator: Operator) => {
+ const contextData = handleContext || {
+ nodeId: '',
+ id: '',
+ type: 'source' as const,
+ position: Position.Right,
+ isFromConnectionDrag: true,
+ };
+
+ const mockEvent = mousePosition
+ ? {
+ clientX: mousePosition.x,
+ clientY: mousePosition.y,
+ }
+ : undefined;
+
+ const newNodeId = addCanvasNode(operator, contextData)(mockEvent);
+
+ if (onNodeCreated && newNodeId) {
+ onNodeCreated(newNodeId);
+ }
+
+ hideModal?.();
+ };
+
+ const renderOperatorItem = (operator: Operator) => {
+ const commonContent = (
+
+
+ {t(`flow.${lowerFirst(operator)}`)}
+
+ );
+
+ return (
+
+
+ {isCustomDropdown ? (
+ handleClick(operator)}>{commonContent}
+ ) : (
+ handleClick(operator)}
+ onSelect={() => hideModal?.()}
+ >
+
+ {t(`flow.${lowerFirst(operator)}`)}
+
+ )}
+
+
+ {t(`flow.${lowerFirst(operator)}Description`)}
+
+
+ );
+ };
+
+ return {operators.map(renderOperatorItem)}
;
+}
+
+function AccordionOperators({
+ isCustomDropdown = false,
+ mousePosition,
+}: {
+ isCustomDropdown?: boolean;
+ mousePosition?: { x: number; y: number };
+}) {
+ return (
+
+
+
+ {t('flow.foundation')}
+
+
+
+
+
+
+
+ {t('flow.dialog')}
+
+
+
+
+
+
+
+ {t('flow.flow')}
+
+
+
+
+
+
+
+ {t('flow.dataManipulation')}
+
+
+
+
+
+
+
+ {t('flow.tools')}
+
+
+
+
+
+
+ );
+}
+
+export function InnerNextStepDropdown({
+ children,
+ hideModal,
+ position,
+ onNodeCreated,
+}: PropsWithChildren &
+ IModalProps & {
+ position?: { x: number; y: number };
+ onNodeCreated?: (newNodeId: string) => void;
+ }) {
+ const dropdownRef = useRef(null);
+
+ useEffect(() => {
+ if (position && hideModal) {
+ const handleKeyDown = (event: KeyboardEvent) => {
+ if (event.key === 'Escape') {
+ hideModal();
+ }
+ };
+
+ document.addEventListener('keydown', handleKeyDown);
+
+ return () => {
+ document.removeEventListener('keydown', handleKeyDown);
+ };
+ }
+ }, [position, hideModal]);
+
+ if (position) {
+ return (
+ e.stopPropagation()}
+ >
+
+
+ );
+ }
+
+ return (
+ {
+ if (!open && hideModal) {
+ hideModal();
+ }
+ }}
+ >
+ {children}
+ e.stopPropagation()}
+ className="w-[300px] font-semibold"
+ >
+ {t('flow.nextStep')}
+
+
+
+
+
+ );
+}
+
+export const NextStepDropdown = memo(InnerNextStepDropdown);
diff --git a/web/src/pages/data-flow/canvas/node/email-node.tsx b/web/src/pages/data-flow/canvas/node/email-node.tsx
new file mode 100644
index 000000000..9482194f3
--- /dev/null
+++ b/web/src/pages/data-flow/canvas/node/email-node.tsx
@@ -0,0 +1,80 @@
+import { IEmailNode } from '@/interfaces/database/flow';
+import { Handle, NodeProps, Position } from '@xyflow/react';
+import { Flex } from 'antd';
+import classNames from 'classnames';
+import { memo, useState } from 'react';
+import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
+import styles from './index.less';
+import NodeHeader from './node-header';
+
+export function InnerEmailNode({
+ id,
+ data,
+ isConnectable = true,
+ selected,
+}: NodeProps) {
+ const [showDetails, setShowDetails] = useState(false);
+
+ return (
+
+
+
+
+
+
+ setShowDetails(!showDetails)}
+ >
+
+ SMTP:
+ {data.form?.smtp_server}
+
+
+ Port:
+ {data.form?.smtp_port}
+
+
+ From:
+ {data.form?.email}
+
+
{showDetails ? '▼' : '▶'}
+
+
+ {showDetails && (
+
+
Expected Input JSON:
+
+ {`{
+ "to_email": "...",
+ "cc_email": "...",
+ "subject": "...",
+ "content": "..."
+}`}
+
+
+ )}
+
+
+ );
+}
+
+export const EmailNode = memo(InnerEmailNode);
diff --git a/web/src/pages/data-flow/canvas/node/generate-node.tsx b/web/src/pages/data-flow/canvas/node/generate-node.tsx
new file mode 100644
index 000000000..8ffbbd79c
--- /dev/null
+++ b/web/src/pages/data-flow/canvas/node/generate-node.tsx
@@ -0,0 +1,60 @@
+import LLMLabel from '@/components/llm-select/llm-label';
+import { useTheme } from '@/components/theme-provider';
+import { IGenerateNode } from '@/interfaces/database/flow';
+import { Handle, NodeProps, Position } from '@xyflow/react';
+import classNames from 'classnames';
+import { get } from 'lodash';
+import { memo } from 'react';
+import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
+import styles from './index.less';
+import NodeHeader from './node-header';
+
+export function InnerGenerateNode({
+ id,
+ data,
+ isConnectable = true,
+ selected,
+}: NodeProps) {
+ const { theme } = useTheme();
+ return (
+
+ );
+}
+
+export const GenerateNode = memo(InnerGenerateNode);
diff --git a/web/src/pages/data-flow/canvas/node/handle-icon.tsx b/web/src/pages/data-flow/canvas/node/handle-icon.tsx
new file mode 100644
index 000000000..36c7f3634
--- /dev/null
+++ b/web/src/pages/data-flow/canvas/node/handle-icon.tsx
@@ -0,0 +1,20 @@
+import { PlusOutlined } from '@ant-design/icons';
+import { CSSProperties } from 'react';
+
+export const HandleIcon = () => {
+ return (
+
+ );
+};
+
+export const RightHandleStyle: CSSProperties = {
+ right: 0,
+};
+
+export const LeftHandleStyle: CSSProperties = {
+ left: 0,
+};
+
+export default HandleIcon;
diff --git a/web/src/pages/data-flow/canvas/node/handle.tsx b/web/src/pages/data-flow/canvas/node/handle.tsx
new file mode 100644
index 000000000..71b473cc7
--- /dev/null
+++ b/web/src/pages/data-flow/canvas/node/handle.tsx
@@ -0,0 +1,64 @@
+import { useSetModalState } from '@/hooks/common-hooks';
+import { cn } from '@/lib/utils';
+import { Handle, HandleProps } from '@xyflow/react';
+import { Plus } from 'lucide-react';
+import { useMemo } from 'react';
+import { HandleContext } from '../../context';
+import { useDropdownManager } from '../context';
+import { InnerNextStepDropdown } from './dropdown/next-step-dropdown';
+
+export function CommonHandle({
+ className,
+ nodeId,
+ ...props
+}: HandleProps & { nodeId: string }) {
+ const { visible, hideModal, showModal } = useSetModalState();
+
+ const { canShowDropdown, setActiveDropdown, clearActiveDropdown } =
+ useDropdownManager();
+
+ const value = useMemo(
+ () => ({
+ nodeId,
+ id: props.id || undefined,
+ type: props.type,
+ position: props.position,
+ isFromConnectionDrag: false,
+ }),
+ [nodeId, props.id, props.position, props.type],
+ );
+
+ return (
+
+ {
+ e.stopPropagation();
+
+ if (!canShowDropdown()) {
+ return;
+ }
+
+ setActiveDropdown('handle');
+ showModal();
+ }}
+ >
+
+ {visible && (
+ {
+ hideModal();
+ clearActiveDropdown();
+ }}
+ >
+
+
+ )}
+
+
+ );
+}
diff --git a/web/src/pages/data-flow/canvas/node/index.less b/web/src/pages/data-flow/canvas/node/index.less
new file mode 100644
index 000000000..14d7e6077
--- /dev/null
+++ b/web/src/pages/data-flow/canvas/node/index.less
@@ -0,0 +1,285 @@
+.dark {
+ background: rgb(63, 63, 63) !important;
+}
+.ragNode {
+ .commonNode();
+ .nodeName {
+ font-size: 10px;
+ color: black;
+ }
+ label {
+ display: block;
+ color: #777;
+ font-size: 12px;
+ }
+ .description {
+ font-size: 10px;
+ }
+
+ .categorizeAnchorPointText {
+ position: absolute;
+ top: -4px;
+ left: 8px;
+ white-space: nowrap;
+ }
+}
+
+@lightBackgroundColor: rgba(150, 150, 150, 0.1);
+@darkBackgroundColor: rgba(150, 150, 150, 0.2);
+
+.selectedNode {
+ 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;
+ justify-content: center;
+ width: 12px;
+ height: 12px;
+ background: rgb(59, 88, 253);
+ border: 1px solid white;
+ z-index: 1;
+ background-image: url('@/assets/svg/plus.svg');
+ background-size: cover;
+ background-position: center;
+}
+
+.jsonView {
+ word-wrap: break-word;
+ overflow: auto;
+ max-width: 300px;
+ max-height: 500px;
+}
+
+.logicNode {
+ .commonNode();
+
+ .nodeName {
+ font-size: 10px;
+ color: black;
+ }
+ label {
+ display: block;
+ color: #777;
+ font-size: 12px;
+ }
+
+ .description {
+ font-size: 10px;
+ }
+
+ .categorizeAnchorPointText {
+ position: absolute;
+ top: -4px;
+ left: 8px;
+ white-space: nowrap;
+ }
+ .relevantSourceLabel {
+ font-size: 10px;
+ }
+}
+
+.noteNode {
+ .commonNode();
+ min-width: 140px;
+ width: auto;
+ height: 100%;
+ padding: 8px;
+ border-radius: 10px;
+ min-height: 128px;
+ .noteTitle {
+ background-color: #edfcff;
+ font-size: 12px;
+ padding: 6px 6px 4px;
+ border-top-left-radius: 10px;
+ border-top-right-radius: 10px;
+ }
+ .noteTitleDark {
+ background-color: #edfcff;
+ font-size: 12px;
+ padding: 6px 6px 4px;
+ border-top-left-radius: 10px;
+ border-top-right-radius: 10px;
+ }
+ .noteForm {
+ margin-top: 4px;
+ height: calc(100% - 50px);
+ }
+ .noteName {
+ padding: 0px 4px;
+ }
+ .noteTextarea {
+ resize: none;
+ border: 0;
+ border-radius: 0;
+ height: 100%;
+ &:focus {
+ border: none;
+ box-shadow: none;
+ }
+ }
+}
+
+.iterationNode {
+ .commonNodeShadow();
+ border-bottom-left-radius: 10px;
+ border-bottom-right-radius: 10px;
+}
+
+.nodeText {
+ padding-inline: 0.4em;
+ padding-block: 0.2em 0.1em;
+ background: @lightBackgroundColor;
+ border-radius: 3px;
+ min-height: 22px;
+ .textEllipsis();
+}
+
+.nodeHeader {
+ padding-bottom: 12px;
+}
+
+.zeroDivider {
+ margin: 0 !important;
+}
+
+.conditionBlock {
+ border-radius: 4px;
+ padding: 6px;
+ background: @lightBackgroundColor;
+}
+
+.conditionLine {
+ border-radius: 4px;
+ padding: 0 4px;
+ background: @darkBackgroundColor;
+ .textEllipsis();
+}
+
+.conditionKey {
+ flex: 1;
+}
+
+.conditionOperator {
+ padding: 0 2px;
+ text-align: center;
+}
+
+.relevantLabel {
+ text-align: right;
+}
+
+.knowledgeNodeName {
+ .textEllipsis();
+}
+
+.messageNodeContainer {
+ overflow-y: auto;
+ max-height: 300px;
+}
+
+.generateParameters {
+ padding-top: 8px;
+ label {
+ flex: 2;
+ .textEllipsis();
+ }
+ .parameterValue {
+ flex: 3;
+ .conditionLine;
+ }
+}
+
+.emailNodeContainer {
+ padding: 8px;
+ font-size: 12px;
+
+ .emailConfig {
+ background: rgba(0, 0, 0, 0.02);
+ border-radius: 4px;
+ padding: 8px;
+ position: relative;
+ cursor: pointer;
+
+ &:hover {
+ background: rgba(0, 0, 0, 0.04);
+ }
+
+ .configItem {
+ display: flex;
+ align-items: center;
+ margin-bottom: 4px;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+
+ .configLabel {
+ color: #666;
+ width: 45px;
+ flex-shrink: 0;
+ }
+
+ .configValue {
+ color: #333;
+ word-break: break-all;
+ }
+ }
+
+ .expandIcon {
+ position: absolute;
+ right: 8px;
+ top: 50%;
+ transform: translateY(-50%);
+ color: #666;
+ font-size: 12px;
+ }
+ }
+
+ .jsonExample {
+ background: #f5f5f5;
+ border-radius: 4px;
+ padding: 8px;
+ margin-top: 4px;
+ animation: slideDown 0.2s ease-out;
+
+ .jsonTitle {
+ color: #666;
+ margin-bottom: 4px;
+ }
+
+ .jsonContent {
+ margin: 0;
+ color: #333;
+ font-family: monospace;
+ }
+ }
+}
+
+@keyframes slideDown {
+ from {
+ opacity: 0;
+ transform: translateY(-10px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
diff --git a/web/src/pages/data-flow/canvas/node/index.tsx b/web/src/pages/data-flow/canvas/node/index.tsx
new file mode 100644
index 000000000..a1d48955b
--- /dev/null
+++ b/web/src/pages/data-flow/canvas/node/index.tsx
@@ -0,0 +1,49 @@
+import { IRagNode } from '@/interfaces/database/flow';
+import { NodeProps, Position } from '@xyflow/react';
+import { memo } from 'react';
+import { NodeHandleId } from '../../constant';
+import { needsSingleStepDebugging } from '../../utils';
+import { CommonHandle } from './handle';
+import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
+import NodeHeader from './node-header';
+import { NodeWrapper } from './node-wrapper';
+import { ToolBar } from './toolbar';
+
+function InnerRagNode({
+ id,
+ data,
+ isConnectable = true,
+ selected,
+}: NodeProps) {
+ return (
+
+
+
+
+
+
+
+ );
+}
+
+export const RagNode = memo(InnerRagNode);
diff --git a/web/src/pages/data-flow/canvas/node/invoke-node.tsx b/web/src/pages/data-flow/canvas/node/invoke-node.tsx
new file mode 100644
index 000000000..cf1e28d02
--- /dev/null
+++ b/web/src/pages/data-flow/canvas/node/invoke-node.tsx
@@ -0,0 +1,62 @@
+import { useTheme } from '@/components/theme-provider';
+import { IInvokeNode } from '@/interfaces/database/flow';
+import { Handle, NodeProps, Position } from '@xyflow/react';
+import { Flex } from 'antd';
+import classNames from 'classnames';
+import { get } from 'lodash';
+import { memo } from 'react';
+import { useTranslation } from 'react-i18next';
+import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
+import styles from './index.less';
+import NodeHeader from './node-header';
+
+function InnerInvokeNode({
+ id,
+ data,
+ isConnectable = true,
+ selected,
+}: NodeProps) {
+ const { t } = useTranslation();
+ const { theme } = useTheme();
+ const url = get(data, 'form.url');
+ return (
+
+
+
+
+
+ {t('flow.url')}
+ {url}
+
+
+ );
+}
+
+export const InvokeNode = memo(InnerInvokeNode);
diff --git a/web/src/pages/data-flow/canvas/node/iteration-node.tsx b/web/src/pages/data-flow/canvas/node/iteration-node.tsx
new file mode 100644
index 000000000..3bdbae590
--- /dev/null
+++ b/web/src/pages/data-flow/canvas/node/iteration-node.tsx
@@ -0,0 +1,93 @@
+import {
+ IIterationNode,
+ IIterationStartNode,
+} from '@/interfaces/database/flow';
+import { cn } from '@/lib/utils';
+import { NodeProps, NodeResizeControl, Position } from '@xyflow/react';
+import { memo } from 'react';
+import { NodeHandleId, Operator } from '../../constant';
+import OperatorIcon from '../../operator-icon';
+import { CommonHandle } from './handle';
+import { RightHandleStyle } from './handle-icon';
+import styles from './index.less';
+import NodeHeader from './node-header';
+import { NodeWrapper } from './node-wrapper';
+import { ResizeIcon, controlStyle } from './resize-icon';
+import { ToolBar } from './toolbar';
+
+export function InnerIterationNode({
+ id,
+ data,
+ isConnectable = true,
+ selected,
+}: NodeProps) {
+ return (
+
+
+
+ );
+}
+
+function InnerIterationStartNode({
+ isConnectable = true,
+ id,
+ selected,
+}: NodeProps) {
+ return (
+
+
+
+
+
+
+ );
+}
+
+export const IterationStartNode = memo(InnerIterationStartNode);
+
+export const IterationNode = memo(InnerIterationNode);
diff --git a/web/src/pages/data-flow/canvas/node/keyword-node.tsx b/web/src/pages/data-flow/canvas/node/keyword-node.tsx
new file mode 100644
index 000000000..012dcf26c
--- /dev/null
+++ b/web/src/pages/data-flow/canvas/node/keyword-node.tsx
@@ -0,0 +1,60 @@
+import LLMLabel from '@/components/llm-select/llm-label';
+import { useTheme } from '@/components/theme-provider';
+import { IKeywordNode } from '@/interfaces/database/flow';
+import { Handle, NodeProps, Position } from '@xyflow/react';
+import classNames from 'classnames';
+import { get } from 'lodash';
+import { memo } from 'react';
+import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
+import styles from './index.less';
+import NodeHeader from './node-header';
+
+export function InnerKeywordNode({
+ id,
+ data,
+ isConnectable = true,
+ selected,
+}: NodeProps) {
+ const { theme } = useTheme();
+ return (
+
+ );
+}
+
+export const KeywordNode = memo(InnerKeywordNode);
diff --git a/web/src/pages/data-flow/canvas/node/logic-node.tsx b/web/src/pages/data-flow/canvas/node/logic-node.tsx
new file mode 100644
index 000000000..481c26c25
--- /dev/null
+++ b/web/src/pages/data-flow/canvas/node/logic-node.tsx
@@ -0,0 +1,41 @@
+import { ILogicNode } from '@/interfaces/database/flow';
+import { NodeProps, Position } from '@xyflow/react';
+import { memo } from 'react';
+import { CommonHandle } from './handle';
+import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
+import NodeHeader from './node-header';
+import { NodeWrapper } from './node-wrapper';
+import { ToolBar } from './toolbar';
+
+export function InnerLogicNode({
+ id,
+ data,
+ isConnectable = true,
+ selected,
+}: NodeProps) {
+ return (
+
+
+
+
+
+
+
+ );
+}
+
+export const LogicNode = memo(InnerLogicNode);
diff --git a/web/src/pages/data-flow/canvas/node/message-node.tsx b/web/src/pages/data-flow/canvas/node/message-node.tsx
new file mode 100644
index 000000000..057845a63
--- /dev/null
+++ b/web/src/pages/data-flow/canvas/node/message-node.tsx
@@ -0,0 +1,65 @@
+import { IMessageNode } from '@/interfaces/database/flow';
+import { NodeProps, Position } from '@xyflow/react';
+import { Flex } from 'antd';
+import classNames from 'classnames';
+import { get } from 'lodash';
+import { memo } from 'react';
+import { NodeHandleId } from '../../constant';
+import { CommonHandle } from './handle';
+import { LeftHandleStyle } from './handle-icon';
+import styles from './index.less';
+import NodeHeader from './node-header';
+import { NodeWrapper } from './node-wrapper';
+import { ToolBar } from './toolbar';
+
+function InnerMessageNode({
+ id,
+ data,
+ isConnectable = true,
+ selected,
+}: NodeProps) {
+ const messages: string[] = get(data, 'form.messages', []);
+ return (
+
+
+
+ {/* */}
+ 0,
+ })}
+ >
+
+
+ {messages.map((message, idx) => {
+ return (
+
+ {message}
+
+ );
+ })}
+
+
+
+ );
+}
+
+export const MessageNode = memo(InnerMessageNode);
diff --git a/web/src/pages/data-flow/canvas/node/node-header.tsx b/web/src/pages/data-flow/canvas/node/node-header.tsx
new file mode 100644
index 000000000..9647af1ed
--- /dev/null
+++ b/web/src/pages/data-flow/canvas/node/node-header.tsx
@@ -0,0 +1,34 @@
+import { cn } from '@/lib/utils';
+import { memo } from 'react';
+import { Operator } from '../../constant';
+import OperatorIcon from '../../operator-icon';
+interface IProps {
+ id: string;
+ label: string;
+ name: string;
+ gap?: number;
+ className?: string;
+ wrapperClassName?: string;
+}
+
+const InnerNodeHeader = ({
+ label,
+ name,
+ className,
+ wrapperClassName,
+}: IProps) => {
+ return (
+
+ );
+};
+
+const NodeHeader = memo(InnerNodeHeader);
+
+export default NodeHeader;
diff --git a/web/src/pages/data-flow/canvas/node/node-wrapper.tsx b/web/src/pages/data-flow/canvas/node/node-wrapper.tsx
new file mode 100644
index 000000000..ab53466ff
--- /dev/null
+++ b/web/src/pages/data-flow/canvas/node/node-wrapper.tsx
@@ -0,0 +1,18 @@
+import { cn } from '@/lib/utils';
+import { HTMLAttributes } from 'react';
+
+type IProps = HTMLAttributes & { selected?: boolean };
+
+export function NodeWrapper({ children, className, selected }: IProps) {
+ return (
+
+ );
+}
diff --git a/web/src/pages/data-flow/canvas/node/note-node/index.tsx b/web/src/pages/data-flow/canvas/node/note-node/index.tsx
new file mode 100644
index 000000000..2c9d2446e
--- /dev/null
+++ b/web/src/pages/data-flow/canvas/node/note-node/index.tsx
@@ -0,0 +1,104 @@
+import { NodeProps, NodeResizeControl } from '@xyflow/react';
+
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormMessage,
+} from '@/components/ui/form';
+import { Input } from '@/components/ui/input';
+import { Textarea } from '@/components/ui/textarea';
+import { INoteNode } from '@/interfaces/database/flow';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { NotebookPen } from 'lucide-react';
+import { memo } from 'react';
+import { useForm } from 'react-hook-form';
+import { useTranslation } from 'react-i18next';
+import { z } from 'zod';
+import { NodeWrapper } from '../node-wrapper';
+import { ResizeIcon, controlStyle } from '../resize-icon';
+import { useWatchFormChange, useWatchNameFormChange } from './use-watch-change';
+
+const FormSchema = z.object({
+ text: z.string(),
+});
+
+const NameFormSchema = z.object({
+ name: z.string(),
+});
+
+function NoteNode({ data, id, selected }: NodeProps) {
+ const { t } = useTranslation();
+
+ const form = useForm>({
+ resolver: zodResolver(FormSchema),
+ defaultValues: data.form,
+ });
+
+ const nameForm = useForm>({
+ resolver: zodResolver(NameFormSchema),
+ defaultValues: { name: data.name },
+ });
+
+ useWatchFormChange(id, form);
+
+ useWatchNameFormChange(id, nameForm);
+
+ return (
+
+
+
+
+
+
+
+
+ );
+}
+
+export default memo(NoteNode);
diff --git a/web/src/pages/data-flow/canvas/node/note-node/use-watch-change.ts b/web/src/pages/data-flow/canvas/node/note-node/use-watch-change.ts
new file mode 100644
index 000000000..9531bda11
--- /dev/null
+++ b/web/src/pages/data-flow/canvas/node/note-node/use-watch-change.ts
@@ -0,0 +1,30 @@
+import useGraphStore from '@/pages/agent/store';
+import { useEffect } from 'react';
+import { UseFormReturn, useWatch } from 'react-hook-form';
+
+export function useWatchFormChange(id?: string, form?: UseFormReturn) {
+ let values = useWatch({ control: form?.control });
+ const updateNodeForm = useGraphStore((state) => state.updateNodeForm);
+
+ useEffect(() => {
+ // Manually triggered form updates are synchronized to the canvas
+ if (id) {
+ values = form?.getValues() || {};
+ let nextValues: any = values;
+
+ updateNodeForm(id, nextValues);
+ }
+ }, [id, updateNodeForm, values]);
+}
+
+export function useWatchNameFormChange(id?: string, form?: UseFormReturn) {
+ let values = useWatch({ control: form?.control });
+ const updateNodeName = useGraphStore((state) => state.updateNodeName);
+
+ useEffect(() => {
+ // Manually triggered form updates are synchronized to the canvas
+ if (id) {
+ updateNodeName(id, values.name);
+ }
+ }, [id, updateNodeName, values]);
+}
diff --git a/web/src/pages/data-flow/canvas/node/popover.tsx b/web/src/pages/data-flow/canvas/node/popover.tsx
new file mode 100644
index 000000000..d44538656
--- /dev/null
+++ b/web/src/pages/data-flow/canvas/node/popover.tsx
@@ -0,0 +1,121 @@
+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 { useReplaceIdWithText } from '../../hooks';
+
+import { useTheme } from '@/components/theme-provider';
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from '@/components/ui/popover';
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from '@/components/ui/table';
+import { useTranslate } from '@/hooks/common-hooks';
+import { useFetchAgent } from '@/hooks/use-agent-request';
+import { useGetComponentLabelByValue } from '../../hooks/use-get-begin-query';
+
+interface IProps extends React.PropsWithChildren {
+ nodeId: string;
+ name?: string;
+}
+
+export function NextNodePopover({ children, nodeId, name }: IProps) {
+ const { t } = useTranslate('flow');
+
+ const { data } = useFetchAgent();
+ const { theme } = useTheme();
+ const component = useMemo(() => {
+ return get(data, ['dsl', 'components', nodeId], {});
+ }, [nodeId, data]);
+
+ const inputs: Array<{ component_id: string; content: string }> = get(
+ component,
+ ['obj', 'inputs'],
+ [],
+ );
+ const output = get(component, ['obj', 'output'], {});
+ const { replacedOutput } = useReplaceIdWithText(output);
+ const stopPropagation: MouseEventHandler = useCallback((e) => {
+ e.stopPropagation();
+ }, []);
+
+ const getLabel = useGetComponentLabelByValue(nodeId);
+
+ return (
+
+
+ {children}
+
+
+
+ {name} {t('operationResults')}
+
+
+
+
{t('input')}
+
+
+
+
+ {t('componentId')}
+ {t('content')}
+
+
+
+ {inputs.map((x, idx) => (
+
+ {getLabel(x.component_id)}
+ {x.content}
+
+ ))}
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/web/src/pages/data-flow/canvas/node/relevant-node.tsx b/web/src/pages/data-flow/canvas/node/relevant-node.tsx
new file mode 100644
index 000000000..410a7accd
--- /dev/null
+++ b/web/src/pages/data-flow/canvas/node/relevant-node.tsx
@@ -0,0 +1,73 @@
+import { Handle, NodeProps, Position } from '@xyflow/react';
+import { Flex } from 'antd';
+import classNames from 'classnames';
+import { RightHandleStyle } from './handle-icon';
+
+import { useTheme } from '@/components/theme-provider';
+import { IRelevantNode } from '@/interfaces/database/flow';
+import { get } from 'lodash';
+import { memo } from 'react';
+import { useReplaceIdWithName } from '../../hooks';
+import styles from './index.less';
+import NodeHeader from './node-header';
+
+function InnerRelevantNode({ id, data, selected }: NodeProps) {
+ const yes = get(data, 'form.yes');
+ const no = get(data, 'form.no');
+ const replaceIdWithName = useReplaceIdWithName();
+ const { theme } = useTheme();
+ return (
+
+
+
+
+
+
+
+
+ Yes
+ {replaceIdWithName(yes)}
+
+
+ No
+ {replaceIdWithName(no)}
+
+
+
+ );
+}
+
+export const RelevantNode = memo(InnerRelevantNode);
diff --git a/web/src/pages/data-flow/canvas/node/resize-icon.tsx b/web/src/pages/data-flow/canvas/node/resize-icon.tsx
new file mode 100644
index 000000000..e01188924
--- /dev/null
+++ b/web/src/pages/data-flow/canvas/node/resize-icon.tsx
@@ -0,0 +1,32 @@
+export function ResizeIcon() {
+ return (
+
+ );
+}
+
+export const controlStyle = {
+ background: 'transparent',
+ border: 'none',
+ cursor: 'nwse-resize',
+};
diff --git a/web/src/pages/data-flow/canvas/node/retrieval-node.tsx b/web/src/pages/data-flow/canvas/node/retrieval-node.tsx
new file mode 100644
index 000000000..5703ec1f4
--- /dev/null
+++ b/web/src/pages/data-flow/canvas/node/retrieval-node.tsx
@@ -0,0 +1,84 @@
+import { RAGFlowAvatar } from '@/components/ragflow-avatar';
+import { useFetchKnowledgeList } from '@/hooks/knowledge-hooks';
+import { IRetrievalNode } from '@/interfaces/database/flow';
+import { NodeProps, Position } from '@xyflow/react';
+import classNames from 'classnames';
+import { get } from 'lodash';
+import { memo } from 'react';
+import { NodeHandleId } from '../../constant';
+import { useGetVariableLabelByValue } from '../../hooks/use-get-begin-query';
+import { CommonHandle } from './handle';
+import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
+import styles from './index.less';
+import NodeHeader from './node-header';
+import { NodeWrapper } from './node-wrapper';
+import { ToolBar } from './toolbar';
+
+function InnerRetrievalNode({
+ id,
+ data,
+ isConnectable = true,
+ selected,
+}: NodeProps) {
+ const knowledgeBaseIds: string[] = get(data, 'form.kb_ids', []);
+ const { list: knowledgeList } = useFetchKnowledgeList(true);
+
+ const getLabel = useGetVariableLabelByValue(id);
+
+ return (
+
+
+
+
+ 0,
+ })}
+ >
+
+ {knowledgeBaseIds.map((id) => {
+ const item = knowledgeList.find((y) => id === y.id);
+ const label = getLabel(id);
+
+ return (
+
+
+
+
+
{label || item?.name}
+
+
+ );
+ })}
+
+
+
+ );
+}
+
+export const RetrievalNode = memo(InnerRetrievalNode);
diff --git a/web/src/pages/data-flow/canvas/node/rewrite-node.tsx b/web/src/pages/data-flow/canvas/node/rewrite-node.tsx
new file mode 100644
index 000000000..134899c8b
--- /dev/null
+++ b/web/src/pages/data-flow/canvas/node/rewrite-node.tsx
@@ -0,0 +1,60 @@
+import LLMLabel from '@/components/llm-select/llm-label';
+import { useTheme } from '@/components/theme-provider';
+import { IRewriteNode } from '@/interfaces/database/flow';
+import { Handle, NodeProps, Position } from '@xyflow/react';
+import classNames from 'classnames';
+import { get } from 'lodash';
+import { memo } from 'react';
+import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
+import styles from './index.less';
+import NodeHeader from './node-header';
+
+function InnerRewriteNode({
+ id,
+ data,
+ isConnectable = true,
+ selected,
+}: NodeProps) {
+ const { theme } = useTheme();
+ return (
+
+ );
+}
+
+export const RewriteNode = memo(InnerRewriteNode);
diff --git a/web/src/pages/data-flow/canvas/node/switch-node.tsx b/web/src/pages/data-flow/canvas/node/switch-node.tsx
new file mode 100644
index 000000000..b62e45adb
--- /dev/null
+++ b/web/src/pages/data-flow/canvas/node/switch-node.tsx
@@ -0,0 +1,118 @@
+import { Card, CardContent } from '@/components/ui/card';
+import { ISwitchCondition, ISwitchNode } from '@/interfaces/database/flow';
+import { NodeProps, Position } from '@xyflow/react';
+import { memo, useCallback } from 'react';
+import { NodeHandleId, SwitchOperatorOptions } from '../../constant';
+import { LogicalOperatorIcon } from '../../form/switch-form';
+import { useGetVariableLabelByValue } from '../../hooks/use-get-begin-query';
+import { CommonHandle } from './handle';
+import { RightHandleStyle } from './handle-icon';
+import NodeHeader from './node-header';
+import { NodeWrapper } from './node-wrapper';
+import { ToolBar } from './toolbar';
+import { useBuildSwitchHandlePositions } from './use-build-switch-handle-positions';
+
+const getConditionKey = (idx: number, length: number) => {
+ if (idx === 0 && length !== 1) {
+ return 'If';
+ } else if (idx === length - 1) {
+ return 'Else';
+ }
+
+ return 'ElseIf';
+};
+
+const ConditionBlock = ({
+ condition,
+ nodeId,
+}: { condition: ISwitchCondition } & { nodeId: string }) => {
+ const items = condition?.items ?? [];
+ const getLabel = useGetVariableLabelByValue(nodeId);
+
+ const renderOperatorIcon = useCallback((operator?: string) => {
+ const item = SwitchOperatorOptions.find((x) => x.value === operator);
+ if (item) {
+ return (
+
+ );
+ }
+ return <>>;
+ }, []);
+
+ return (
+
+
+ {items.map((x, idx) => (
+
+
+
+ {getLabel(x?.cpn_id)}
+
+ {renderOperatorIcon(x?.operator)}
+ {x?.value}
+
+
+ ))}
+
+
+ );
+};
+
+function InnerSwitchNode({ id, data, selected }: NodeProps) {
+ const { positions } = useBuildSwitchHandlePositions({ data, id });
+ return (
+
+
+
+
+
+ {positions.map((position, idx) => {
+ return (
+
+
+
+
{getConditionKey(idx, positions.length)}
+
+ {idx < positions.length - 1 && position.text}
+
+
+
+ {idx < positions.length - 1 &&
+ position.condition?.logical_operator?.toUpperCase()}
+
+ {position.condition && (
+
+ )}
+
+
+
+ );
+ })}
+
+
+
+ );
+}
+
+export const SwitchNode = memo(InnerSwitchNode);
diff --git a/web/src/pages/data-flow/canvas/node/template-node.tsx b/web/src/pages/data-flow/canvas/node/template-node.tsx
new file mode 100644
index 000000000..b204717ab
--- /dev/null
+++ b/web/src/pages/data-flow/canvas/node/template-node.tsx
@@ -0,0 +1,78 @@
+import { useTheme } from '@/components/theme-provider';
+import { Handle, NodeProps, Position } from '@xyflow/react';
+import { Flex } from 'antd';
+import classNames from 'classnames';
+import { get } from 'lodash';
+import { useGetComponentLabelByValue } from '../../hooks/use-get-begin-query';
+import { IGenerateParameter } from '../../interface';
+import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
+import NodeHeader from './node-header';
+
+import { ITemplateNode } from '@/interfaces/database/flow';
+import { memo } from 'react';
+import styles from './index.less';
+
+function InnerTemplateNode({
+ id,
+ data,
+ isConnectable = true,
+ selected,
+}: NodeProps) {
+ const parameters: IGenerateParameter[] = get(data, 'form.parameters', []);
+ const getLabel = useGetComponentLabelByValue(id);
+ const { theme } = useTheme();
+ return (
+
+
+
+
+
+
+
+ {parameters.map((x) => (
+
+
+
+ {getLabel(x.component_id)}
+
+
+ ))}
+
+
+ );
+}
+
+export const TemplateNode = memo(InnerTemplateNode);
diff --git a/web/src/pages/data-flow/canvas/node/tool-node.tsx b/web/src/pages/data-flow/canvas/node/tool-node.tsx
new file mode 100644
index 000000000..42c7bf3b5
--- /dev/null
+++ b/web/src/pages/data-flow/canvas/node/tool-node.tsx
@@ -0,0 +1,83 @@
+import { IAgentForm, IToolNode } from '@/interfaces/database/agent';
+import { Handle, NodeProps, Position } from '@xyflow/react';
+import { get } from 'lodash';
+import { MouseEventHandler, memo, useCallback } from 'react';
+import { NodeHandleId, Operator } from '../../constant';
+import { ToolCard } from '../../form/agent-form/agent-tools';
+import { useFindMcpById } from '../../hooks/use-find-mcp-by-id';
+import OperatorIcon from '../../operator-icon';
+import useGraphStore from '../../store';
+import { NodeWrapper } from './node-wrapper';
+
+function InnerToolNode({
+ id,
+ isConnectable = true,
+ selected,
+}: NodeProps) {
+ const { edges, getNode } = useGraphStore((state) => state);
+ const upstreamAgentNodeId = edges.find((x) => x.target === id)?.source;
+ const upstreamAgentNode = getNode(upstreamAgentNodeId);
+ const { findMcpById } = useFindMcpById();
+
+ const handleClick = useCallback(
+ (operator: string): MouseEventHandler =>
+ (e) => {
+ if (operator === Operator.Code) {
+ e.preventDefault();
+ e.stopPropagation();
+ }
+ },
+ [],
+ );
+
+ const tools: IAgentForm['tools'] = get(
+ upstreamAgentNode,
+ 'data.form.tools',
+ [],
+ );
+
+ const mcpList: IAgentForm['mcp'] = get(
+ upstreamAgentNode,
+ 'data.form.mcp',
+ [],
+ );
+
+ return (
+
+
+
+ {tools.map((x) => (
+
+
+
+ {x.component_name}
+
+
+ ))}
+
+ {mcpList.map((x) => (
+
+ {findMcpById(x.mcp_id)?.name}
+
+ ))}
+
+
+ );
+}
+
+export const ToolNode = memo(InnerToolNode);
diff --git a/web/src/pages/data-flow/canvas/node/toolbar.tsx b/web/src/pages/data-flow/canvas/node/toolbar.tsx
new file mode 100644
index 000000000..a8b5cd13c
--- /dev/null
+++ b/web/src/pages/data-flow/canvas/node/toolbar.tsx
@@ -0,0 +1,88 @@
+import {
+ TooltipContent,
+ TooltipNode,
+ TooltipTrigger,
+} from '@/components/xyflow/tooltip-node';
+import { Position } from '@xyflow/react';
+import { Copy, Play, Trash2 } from 'lucide-react';
+import {
+ HTMLAttributes,
+ MouseEventHandler,
+ PropsWithChildren,
+ useCallback,
+} from 'react';
+import { Operator } from '../../constant';
+import { useDuplicateNode } from '../../hooks';
+import useGraphStore from '../../store';
+
+function IconWrapper({ children, ...props }: HTMLAttributes) {
+ return (
+
+ {children}
+
+ );
+}
+
+type ToolBarProps = {
+ selected?: boolean | undefined;
+ label: string;
+ id: string;
+ showRun?: boolean;
+} & PropsWithChildren;
+
+export function ToolBar({
+ selected,
+ children,
+ label,
+ id,
+ showRun = true,
+}: ToolBarProps) {
+ const deleteNodeById = useGraphStore((store) => store.deleteNodeById);
+ const deleteIterationNodeById = useGraphStore(
+ (store) => store.deleteIterationNodeById,
+ );
+
+ const deleteNode: MouseEventHandler = useCallback(
+ (e) => {
+ e.stopPropagation();
+ if (label === Operator.Iteration) {
+ deleteIterationNodeById(id);
+ } else {
+ deleteNodeById(id);
+ }
+ },
+ [deleteIterationNodeById, deleteNodeById, id, label],
+ );
+
+ const duplicateNode = useDuplicateNode();
+
+ const handleDuplicate: MouseEventHandler = useCallback(
+ (e) => {
+ e.stopPropagation();
+ duplicateNode(id, label);
+ },
+ [duplicateNode, id, label],
+ );
+
+ return (
+
+ {children}
+
+
+
+ {showRun && (
+
+
+
+ )}{' '}
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/web/src/pages/data-flow/canvas/node/use-build-categorize-handle-positions.ts b/web/src/pages/data-flow/canvas/node/use-build-categorize-handle-positions.ts
new file mode 100644
index 000000000..9973441e7
--- /dev/null
+++ b/web/src/pages/data-flow/canvas/node/use-build-categorize-handle-positions.ts
@@ -0,0 +1,48 @@
+import { RAGFlowNodeType } from '@/interfaces/database/flow';
+import { useUpdateNodeInternals } from '@xyflow/react';
+import { get } from 'lodash';
+import { useEffect, useMemo } from 'react';
+import { z } from 'zod';
+import { useCreateCategorizeFormSchema } from '../../form/categorize-form/use-form-schema';
+
+export const useBuildCategorizeHandlePositions = ({
+ data,
+ id,
+}: {
+ id: string;
+ data: RAGFlowNodeType['data'];
+}) => {
+ const updateNodeInternals = useUpdateNodeInternals();
+
+ const FormSchema = useCreateCategorizeFormSchema();
+
+ type FormSchemaType = z.infer;
+
+ const items: Required = useMemo(() => {
+ return get(data, `form.items`, []);
+ }, [data]);
+
+ const positions = useMemo(() => {
+ const list: Array<{
+ top: number;
+ name: string;
+ uuid: string;
+ }> &
+ Required = [];
+
+ items.forEach((x, idx) => {
+ list.push({
+ ...x,
+ top: idx === 0 ? 86 : list[idx - 1].top + 8 + 24,
+ });
+ });
+
+ return list;
+ }, [items]);
+
+ useEffect(() => {
+ updateNodeInternals(id);
+ }, [id, updateNodeInternals, items]);
+
+ return { positions };
+};
diff --git a/web/src/pages/data-flow/canvas/node/use-build-switch-handle-positions.ts b/web/src/pages/data-flow/canvas/node/use-build-switch-handle-positions.ts
new file mode 100644
index 000000000..ee221bf82
--- /dev/null
+++ b/web/src/pages/data-flow/canvas/node/use-build-switch-handle-positions.ts
@@ -0,0 +1,59 @@
+import { ISwitchCondition, RAGFlowNodeType } from '@/interfaces/database/flow';
+import { useUpdateNodeInternals } from '@xyflow/react';
+import get from 'lodash/get';
+import { useEffect, useMemo } from 'react';
+import { SwitchElseTo } from '../../constant';
+import { generateSwitchHandleText } from '../../utils';
+
+export const useBuildSwitchHandlePositions = ({
+ data,
+ id,
+}: {
+ id: string;
+ data: RAGFlowNodeType['data'];
+}) => {
+ const updateNodeInternals = useUpdateNodeInternals();
+
+ const conditions: ISwitchCondition[] = useMemo(() => {
+ return get(data, 'form.conditions', []);
+ }, [data]);
+
+ const positions = useMemo(() => {
+ const list: Array<{
+ text: string;
+ top: number;
+ idx: number;
+ condition?: ISwitchCondition;
+ }> = [];
+
+ [...conditions, ''].forEach((x, idx) => {
+ let top = idx === 0 ? 53 : list[idx - 1].top + 10 + 14 + 16 + 16; // case number (Case 1) height + flex gap
+ if (idx >= 1) {
+ const previousItems = conditions[idx - 1]?.items ?? [];
+ if (previousItems.length > 0) {
+ // top += 12; // ConditionBlock padding
+ top += previousItems.length * 26; // condition variable height
+ // top += (previousItems.length - 1) * 25; // operator height
+ }
+ }
+
+ list.push({
+ text:
+ idx < conditions.length
+ ? generateSwitchHandleText(idx)
+ : SwitchElseTo,
+ idx,
+ top,
+ condition: typeof x === 'string' ? undefined : x,
+ });
+ });
+
+ return list;
+ }, [conditions]);
+
+ useEffect(() => {
+ updateNodeInternals(id);
+ }, [id, updateNodeInternals, conditions]);
+
+ return { positions };
+};
diff --git a/web/src/pages/data-flow/components/background.tsx b/web/src/pages/data-flow/components/background.tsx
new file mode 100644
index 000000000..cf7b18f45
--- /dev/null
+++ b/web/src/pages/data-flow/components/background.tsx
@@ -0,0 +1,13 @@
+import { useIsDarkTheme } from '@/components/theme-provider';
+import { Background } from '@xyflow/react';
+
+export function AgentBackground() {
+ const isDarkTheme = useIsDarkTheme();
+
+ return (
+
+ );
+}
diff --git a/web/src/pages/data-flow/constant.tsx b/web/src/pages/data-flow/constant.tsx
new file mode 100644
index 000000000..7e57c4abc
--- /dev/null
+++ b/web/src/pages/data-flow/constant.tsx
@@ -0,0 +1,947 @@
+import {
+ initialKeywordsSimilarityWeightValue,
+ initialSimilarityThresholdValue,
+} from '@/components/similarity-slider';
+import {
+ AgentGlobals,
+ CodeTemplateStrMap,
+ ProgrammingLanguage,
+} from '@/constants/agent';
+
+export enum AgentDialogueMode {
+ Conversational = 'conversational',
+ Task = 'task',
+}
+
+import {
+ ChatVariableEnabledField,
+ variableEnabledFieldMap,
+} from '@/constants/chat';
+import { ModelVariableType } from '@/constants/knowledge';
+import i18n from '@/locales/config';
+import { setInitialChatVariableEnabledFieldValue } from '@/utils/chat';
+import { t } from 'i18next';
+
+// DuckDuckGo's channel options
+export enum Channel {
+ Text = 'text',
+ News = 'news',
+}
+
+export enum PromptRole {
+ User = 'user',
+ Assistant = 'assistant',
+}
+
+import {
+ Circle,
+ CircleSlash2,
+ CloudUpload,
+ ListOrdered,
+ OptionIcon,
+ TextCursorInput,
+ ToggleLeft,
+ WrapText,
+} from 'lucide-react';
+
+export const BeginId = 'begin';
+
+export enum Operator {
+ Begin = 'Begin',
+ Retrieval = 'Retrieval',
+ Categorize = 'Categorize',
+ Message = 'Message',
+ Relevant = 'Relevant',
+ RewriteQuestion = 'RewriteQuestion',
+ KeywordExtract = 'KeywordExtract',
+ Baidu = 'Baidu',
+ DuckDuckGo = 'DuckDuckGo',
+ Wikipedia = 'Wikipedia',
+ PubMed = 'PubMed',
+ ArXiv = 'ArXiv',
+ Google = 'Google',
+ Bing = 'Bing',
+ GoogleScholar = 'GoogleScholar',
+ DeepL = 'DeepL',
+ GitHub = 'GitHub',
+ BaiduFanyi = 'BaiduFanyi',
+ QWeather = 'QWeather',
+ ExeSQL = 'ExeSQL',
+ Switch = 'Switch',
+ WenCai = 'WenCai',
+ AkShare = 'AkShare',
+ YahooFinance = 'YahooFinance',
+ Jin10 = 'Jin10',
+ Concentrator = 'Concentrator',
+ TuShare = 'TuShare',
+ Note = 'Note',
+ Crawler = 'Crawler',
+ Invoke = 'Invoke',
+ Email = 'Email',
+ Iteration = 'Iteration',
+ IterationStart = 'IterationItem',
+ Code = 'CodeExec',
+ WaitingDialogue = 'WaitingDialogue',
+ Agent = 'Agent',
+ Tool = 'Tool',
+ TavilySearch = 'TavilySearch',
+ TavilyExtract = 'TavilyExtract',
+ UserFillUp = 'UserFillUp',
+ StringTransform = 'StringTransform',
+ SearXNG = 'SearXNG',
+}
+
+export const SwitchLogicOperatorOptions = ['and', 'or'];
+
+export const CommonOperatorList = Object.values(Operator).filter(
+ (x) => x !== Operator.Note,
+);
+
+export const AgentOperatorList = [
+ Operator.Retrieval,
+ Operator.Categorize,
+ Operator.Message,
+ Operator.RewriteQuestion,
+ Operator.KeywordExtract,
+ Operator.Switch,
+ Operator.Concentrator,
+ Operator.Iteration,
+ Operator.WaitingDialogue,
+ Operator.Note,
+ Operator.Agent,
+];
+
+export const componentMenuList = [
+ {
+ name: Operator.Retrieval,
+ },
+ {
+ name: Operator.Categorize,
+ },
+ {
+ name: Operator.Message,
+ },
+
+ {
+ name: Operator.RewriteQuestion,
+ },
+ {
+ name: Operator.KeywordExtract,
+ },
+ {
+ name: Operator.Switch,
+ },
+ {
+ name: Operator.Concentrator,
+ },
+ {
+ name: Operator.Iteration,
+ },
+ {
+ name: Operator.Code,
+ },
+ {
+ name: Operator.WaitingDialogue,
+ },
+ {
+ name: Operator.Agent,
+ },
+ {
+ name: Operator.Note,
+ },
+ {
+ name: Operator.DuckDuckGo,
+ },
+ {
+ name: Operator.Baidu,
+ },
+ {
+ name: Operator.Wikipedia,
+ },
+ {
+ name: Operator.PubMed,
+ },
+ {
+ name: Operator.ArXiv,
+ },
+ {
+ name: Operator.Google,
+ },
+ {
+ name: Operator.Bing,
+ },
+ {
+ name: Operator.GoogleScholar,
+ },
+ {
+ name: Operator.DeepL,
+ },
+ {
+ name: Operator.GitHub,
+ },
+ {
+ name: Operator.BaiduFanyi,
+ },
+ {
+ name: Operator.QWeather,
+ },
+ {
+ name: Operator.ExeSQL,
+ },
+ {
+ name: Operator.WenCai,
+ },
+ {
+ name: Operator.AkShare,
+ },
+ {
+ name: Operator.YahooFinance,
+ },
+ {
+ name: Operator.Jin10,
+ },
+ {
+ name: Operator.TuShare,
+ },
+ {
+ name: Operator.Crawler,
+ },
+ {
+ name: Operator.Invoke,
+ },
+ {
+ name: Operator.Email,
+ },
+ {
+ name: Operator.SearXNG,
+ },
+];
+
+export const SwitchOperatorOptions = [
+ { value: '=', label: 'equal', icon: 'equal' },
+ { value: '≠', label: 'notEqual', icon: 'not-equals' },
+ { value: '>', label: 'gt', icon: 'Less' },
+ { value: '≥', label: 'ge', icon: 'Greater-or-equal' },
+ { value: '<', label: 'lt', icon: 'Less' },
+ { value: '≤', label: 'le', icon: 'less-or-equal' },
+ { value: 'contains', label: 'contains', icon: 'Contains' },
+ { value: 'not contains', label: 'notContains', icon: 'not-contains' },
+ { value: 'start with', label: 'startWith', icon: 'list-start' },
+ { value: 'end with', label: 'endWith', icon: 'list-end' },
+ {
+ value: 'empty',
+ label: 'empty',
+ icon: ,
+ },
+ {
+ value: 'not empty',
+ label: 'notEmpty',
+ icon: ,
+ },
+];
+
+export const SwitchElseTo = 'end_cpn_ids';
+
+const initialQueryBaseValues = {
+ query: [],
+};
+
+export const initialRetrievalValues = {
+ query: AgentGlobals.SysQuery,
+ top_n: 8,
+ top_k: 1024,
+ kb_ids: [],
+ rerank_id: '',
+ empty_response: '',
+ ...initialSimilarityThresholdValue,
+ ...initialKeywordsSimilarityWeightValue,
+ use_kg: false,
+ cross_languages: [],
+ outputs: {
+ formalized_content: {
+ type: 'string',
+ value: '',
+ },
+ },
+};
+
+export const initialBeginValues = {
+ mode: AgentDialogueMode.Conversational,
+ prologue: `Hi! I'm your assistant. What can I do for you?`,
+};
+
+export const variableCheckBoxFieldMap = Object.keys(
+ variableEnabledFieldMap,
+).reduce>((pre, cur) => {
+ pre[cur] = setInitialChatVariableEnabledFieldValue(
+ cur as ChatVariableEnabledField,
+ );
+ return pre;
+}, {});
+
+const initialLlmBaseValues = {
+ ...variableCheckBoxFieldMap,
+ temperature: 0.1,
+ top_p: 0.3,
+ frequency_penalty: 0.7,
+ presence_penalty: 0.4,
+ max_tokens: 256,
+};
+
+export const initialGenerateValues = {
+ ...initialLlmBaseValues,
+ prompt: i18n.t('flow.promptText'),
+ cite: true,
+ message_history_window_size: 12,
+ parameters: [],
+};
+
+export const initialRewriteQuestionValues = {
+ ...initialLlmBaseValues,
+ language: '',
+ message_history_window_size: 6,
+};
+
+export const initialRelevantValues = {
+ ...initialLlmBaseValues,
+};
+
+export const initialCategorizeValues = {
+ ...initialLlmBaseValues,
+ query: AgentGlobals.SysQuery,
+ parameter: ModelVariableType.Precise,
+ message_history_window_size: 1,
+ items: [],
+ outputs: {
+ category_name: {
+ type: 'string',
+ },
+ },
+};
+
+export const initialMessageValues = {
+ content: [''],
+};
+
+export const initialKeywordExtractValues = {
+ ...initialLlmBaseValues,
+ top_n: 3,
+ ...initialQueryBaseValues,
+};
+export const initialDuckValues = {
+ top_n: 10,
+ channel: Channel.Text,
+ query: AgentGlobals.SysQuery,
+ outputs: {
+ formalized_content: {
+ value: '',
+ type: 'string',
+ },
+ json: {
+ value: [],
+ type: 'Array