Compare commits

...

2 Commits

Author SHA1 Message Date
c49e81882c Feat: Remove the copy icon from the toolbar for the Splitter and Parser nodes #9869 (#10367)
### What problem does this PR solve?
Feat: Remove the copy icon from the toolbar for the Splitter and Parser
nodes #9869

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
2025-09-29 18:55:53 +08:00
63cdce660e Feat: Limit the number of Splitter and Parser operators on the canvas to only one #9869 (#10362)
### What problem does this PR solve?

Feat: Limit the number of Splitter and Parser operators on the canvas to
only one #9869

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
2025-09-29 17:22:40 +08:00
15 changed files with 159 additions and 264 deletions

View File

@ -1687,15 +1687,20 @@ This delimiter is used to split the input text into several text pieces echo of
}, },
dataflow: { dataflow: {
parser: 'Parser', parser: 'Parser',
parserDescription: 'Parser', parserDescription:
chunker: 'Chunker', 'Extracts raw text and structure from files for downstream processing.',
chunkerDescription: 'Chunker',
tokenizer: 'Tokenizer', tokenizer: 'Tokenizer',
tokenizerDescription: 'Tokenizer', tokenizerDescription:
splitter: 'Splitter', 'Transforms text into the required data structure (e.g., vector embeddings for Embedding Search) depending on the chosen search method.',
splitterDescription: 'Splitter', splitter: 'Token Splitter',
hierarchicalMergerDescription: 'Hierarchical merger', splitterDescription:
hierarchicalMerger: 'Hierarchical merger', 'Split text into chunks by token length with optional delimiters and overlap.',
hierarchicalMergerDescription:
'Split documents into sections by title hierarchy with regex rules for finer control.',
hierarchicalMerger: 'Title Splitter',
extractor: 'Context Generator',
extractorDescription:
'Use an LLM to extract structured insights from document chunks—such as summaries, classifications, etc.',
outputFormat: 'Output format', outputFormat: 'Output format',
lang: 'Language', lang: 'Language',
fileFormats: 'File formats', fileFormats: 'File formats',
@ -1713,8 +1718,6 @@ This delimiter is used to split the input text into several text pieces echo of
exportJson: 'Export JSON', exportJson: 'Export JSON',
viewResult: 'View Result', viewResult: 'View Result',
running: 'Running', running: 'Running',
extractor: 'Extractor',
extractorDescription: 'Extractor',
summary: 'Augmented Context', summary: 'Augmented Context',
keywords: 'Keywords', keywords: 'Keywords',
questions: 'Questions', questions: 'Questions',

View File

@ -1605,15 +1605,19 @@ General实体和关系提取提示来自 GitHub - microsoft/graphrag基于
}, },
dataflow: { dataflow: {
parser: '解析器', parser: '解析器',
parserDescription: '解析器', parserDescription: '从文件中提取原始文本和结构以供下游处理。',
chunker: '分块器',
chunkerDescription: '分块器',
tokenizer: '分词器', tokenizer: '分词器',
tokenizerDescription: '分词器', tokenizerDescription:
splitter: '拆分器', '根据所选的搜索方法,将文本转换为所需的数据结构(例如,用于嵌入搜索的向量嵌入)。',
splitterDescription: '拆分器', splitter: '分词器拆分器',
hierarchicalMergerDesription: '分层合并', splitterDescription:
hierarchicalMerger: '分层合并', '根据分词器长度将文本拆分成块,并带有可选的分隔符和重叠。',
hierarchicalMergerDescription:
'使用正则表达式规则按标题层次结构将文档拆分成多个部分,以实现更精细的控制。',
hierarchicalMerger: '标题拆分器',
extractor: '提取器',
extractorDescription:
'使用 LLM 从文档块(例如摘要、分类等)中提取结构化见解。',
outputFormat: '输出格式', outputFormat: '输出格式',
lang: '语言', lang: '语言',
fileFormats: '文件格式', fileFormats: '文件格式',
@ -1632,8 +1636,6 @@ General实体和关系提取提示来自 GitHub - microsoft/graphrag基于
exportJson: '导出 JSON', exportJson: '导出 JSON',
viewResult: '查看结果', viewResult: '查看结果',
running: '运行中', running: '运行中',
extractor: '提取器',
extractorDescription: '提取器',
summary: '增强上下文', summary: '增强上下文',
keywords: '关键词', keywords: '关键词',
questions: '问题', questions: '问题',
@ -1687,6 +1689,7 @@ General实体和关系提取提示来自 GitHub - microsoft/graphrag基于
}, },
}, },
cancel: '取消', cancel: '取消',
filenameEmbeddingWeight: '文件名嵌入权重',
switchPromptMessage: '提示词将发生变化,请确认是否放弃已有提示词?', switchPromptMessage: '提示词将发生变化,请确认是否放弃已有提示词?',
}, },
}, },

View File

@ -11,7 +11,7 @@ import useGraphStore from '../../store';
import { useFetchAgent } from '@/hooks/use-agent-request'; import { useFetchAgent } from '@/hooks/use-agent-request';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { NodeHandleId, Operator } from '../../constant'; import { Operator } from '../../constant';
function InnerButtonEdge({ function InnerButtonEdge({
id, id,
@ -27,7 +27,6 @@ function InnerButtonEdge({
markerEnd, markerEnd,
selected, selected,
data, data,
sourceHandleId,
}: EdgeProps<Edge<{ isHovered: boolean }>>) { }: EdgeProps<Edge<{ isHovered: boolean }>>) {
const deleteEdgeById = useGraphStore((state) => state.deleteEdgeById); const deleteEdgeById = useGraphStore((state) => state.deleteEdgeById);
const [edgePath, labelX, labelY] = getBezierPath({ const [edgePath, labelX, labelY] = getBezierPath({
@ -49,47 +48,32 @@ function InnerButtonEdge({
// highlight the nodes that the workflow passes through // highlight the nodes that the workflow passes through
const { data: flowDetail } = useFetchAgent(); const { data: flowDetail } = useFetchAgent();
const graphPath = useMemo(() => { const showHighlight = useMemo(() => {
// TODO: this will be called multiple times
const path = flowDetail?.dsl?.path ?? []; const path = flowDetail?.dsl?.path ?? [];
// The second to last const idx = path.findIndex((x) => x === target);
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) { if (idx !== -1) {
// The set of elements following source let index = idx - 1;
const slicedGraphPath = graphPath.slice(idx + 1); while (index >= 0) {
if (slicedGraphPath.some((x) => x === target)) { if (path[index] === source) {
return { strokeWidth: 1, stroke: 'red' }; return { strokeWidth: 1, stroke: 'var(--accent-primary)' };
}
index--;
} }
return {};
} }
return {}; return {};
}, [source, target, graphPath]); }, [flowDetail?.dsl?.path, source, target]);
const visible = useMemo(() => { const visible = useMemo(() => {
return ( return data?.isHovered && source !== Operator.Begin;
data?.isHovered && }, [data?.isHovered, source]);
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 ( return (
<> <>
<BaseEdge <BaseEdge
path={edgePath} path={edgePath}
markerEnd={markerEnd} markerEnd={markerEnd}
style={{ ...style, ...selectedStyle, ...highlightStyle }} style={{ ...style, ...selectedStyle, ...showHighlight }}
className="text-text-secondary" className="text-text-secondary"
/> />

View File

@ -12,6 +12,7 @@ import {
ControlButton, ControlButton,
Controls, Controls,
NodeTypes, NodeTypes,
OnConnectEnd,
Position, Position,
ReactFlow, ReactFlow,
ReactFlowInstance, ReactFlowInstance,
@ -36,12 +37,13 @@ import {
useShowDrawer, useShowDrawer,
} from '../hooks/use-show-drawer'; } from '../hooks/use-show-drawer';
import RunSheet from '../run-sheet'; import RunSheet from '../run-sheet';
import useGraphStore from '../store';
import { ButtonEdge } from './edge'; import { ButtonEdge } from './edge';
import styles from './index.less'; import styles from './index.less';
import { RagNode } from './node'; import { RagNode } from './node';
import { BeginNode } from './node/begin-node'; import { BeginNode } from './node/begin-node';
import { ContextNode } from './node/context-node'; import { NextStepDropdown } from './node/dropdown/next-step-dropdown';
import { InnerNextStepDropdown } from './node/dropdown/next-step-dropdown'; import { ExtractorNode } from './node/extractor-node';
import { HierarchicalMergerNode } from './node/hierarchical-merger-node'; import { HierarchicalMergerNode } from './node/hierarchical-merger-node';
import NoteNode from './node/note-node'; import NoteNode from './node/note-node';
import ParserNode from './node/parser-node'; import ParserNode from './node/parser-node';
@ -56,7 +58,7 @@ export const nodeTypes: NodeTypes = {
tokenizerNode: TokenizerNode, tokenizerNode: TokenizerNode,
splitterNode: SplitterNode, splitterNode: SplitterNode,
hierarchicalMergerNode: HierarchicalMergerNode, hierarchicalMergerNode: HierarchicalMergerNode,
contextNode: ContextNode, contextNode: ExtractorNode,
}; };
const edgeTypes = { const edgeTypes = {
@ -116,6 +118,7 @@ function DataFlowCanvas({ drawerVisible, hideDrawer, showLogSheet }: IProps) {
useHideFormSheetOnNodeDeletion({ hideFormDrawer }); useHideFormSheetOnNodeDeletion({ hideFormDrawer });
const { visible, hideModal, showModal } = useSetModalState(); const { visible, hideModal, showModal } = useSetModalState();
const [dropdownPosition, setDropdownPosition] = useState({ x: 0, y: 0 }); const [dropdownPosition, setDropdownPosition] = useState({ x: 0, y: 0 });
const isConnectedRef = useRef(false); const isConnectedRef = useRef(false);
@ -128,6 +131,8 @@ function DataFlowCanvas({ drawerVisible, hideDrawer, showLogSheet }: IProps) {
const { setActiveDropdown, clearActiveDropdown } = useDropdownManager(); const { setActiveDropdown, clearActiveDropdown } = useDropdownManager();
const { hasChildNode } = useGraphStore((state) => state);
const onPaneClick = useCallback(() => { const onPaneClick = useCallback(() => {
hideFormDrawer(); hideFormDrawer();
if (visible && !preventCloseRef.current) { if (visible && !preventCloseRef.current) {
@ -159,7 +164,7 @@ function DataFlowCanvas({ drawerVisible, hideDrawer, showLogSheet }: IProps) {
isConnectedRef.current = true; isConnectedRef.current = true;
}; };
const OnConnectStart = (event: any, params: any) => { const onConnectStart = (event: any, params: any) => {
isConnectedRef.current = false; isConnectedRef.current = false;
if (params && params.nodeId && params.handleId) { if (params && params.nodeId && params.handleId) {
@ -172,7 +177,12 @@ function DataFlowCanvas({ drawerVisible, hideDrawer, showLogSheet }: IProps) {
} }
}; };
const OnConnectEnd = (event: MouseEvent | TouchEvent) => { const onConnectEnd: OnConnectEnd = (event, connectionState) => {
const nodeId = connectionState.fromNode?.id;
// Events triggered by Handle are directly interrupted
if (connectionState.toNode !== null || (nodeId && hasChildNode(nodeId))) {
return;
}
if ('clientX' in event && 'clientY' in event) { if ('clientX' in event && 'clientY' in event) {
const { clientX, clientY } = event; const { clientX, clientY } = event;
setDropdownPosition({ x: clientX, y: clientY }); setDropdownPosition({ x: clientX, y: clientY });
@ -220,8 +230,8 @@ function DataFlowCanvas({ drawerVisible, hideDrawer, showLogSheet }: IProps) {
onConnect={onConnect} onConnect={onConnect}
nodeTypes={nodeTypes} nodeTypes={nodeTypes}
edgeTypes={edgeTypes} edgeTypes={edgeTypes}
onConnectStart={OnConnectStart} onConnectStart={onConnectStart}
onConnectEnd={OnConnectEnd} onConnectEnd={onConnectEnd}
onNodeClick={onNodeClick} onNodeClick={onNodeClick}
onPaneClick={onPaneClick} onPaneClick={onPaneClick}
onInit={setReactFlowInstance} onInit={setReactFlowInstance}
@ -268,7 +278,7 @@ function DataFlowCanvas({ drawerVisible, hideDrawer, showLogSheet }: IProps) {
isFromConnectionDrag: true, isFromConnectionDrag: true,
}} }}
> >
<InnerNextStepDropdown <NextStepDropdown
hideModal={() => { hideModal={() => {
hideModal(); hideModal();
clearActiveDropdown(); clearActiveDropdown();
@ -276,7 +286,7 @@ function DataFlowCanvas({ drawerVisible, hideDrawer, showLogSheet }: IProps) {
position={dropdownPosition} position={dropdownPosition}
> >
<span></span> <span></span>
</InnerNextStepDropdown> </NextStepDropdown>
</HandleContext.Provider> </HandleContext.Provider>
)} )}
</AgentInstanceContext.Provider> </AgentInstanceContext.Provider>
@ -306,7 +316,6 @@ function DataFlowCanvas({ drawerVisible, hideDrawer, showLogSheet }: IProps) {
loading={running} loading={running}
></RunSheet> ></RunSheet>
)} )}
{/* {logSheetVisible && <LogSheet hideModal={hideLogSheet}></LogSheet>} */}
</div> </div>
); );
} }

View File

@ -1 +0,0 @@
export { RagNode as ContextNode } from './index';

View File

@ -12,6 +12,7 @@ import {
} from '@/components/ui/tooltip'; } from '@/components/ui/tooltip';
import { IModalProps } from '@/interfaces/common'; import { IModalProps } from '@/interfaces/common';
import { useGetNodeDescription, useGetNodeName } from '@/pages/data-flow/hooks'; import { useGetNodeDescription, useGetNodeName } from '@/pages/data-flow/hooks';
import useGraphStore from '@/pages/data-flow/store';
import { Position } from '@xyflow/react'; import { Position } from '@xyflow/react';
import { t } from 'i18next'; import { t } from 'i18next';
import { import {
@ -20,9 +21,10 @@ import {
memo, memo,
useContext, useContext,
useEffect, useEffect,
useMemo,
useRef, useRef,
} from 'react'; } from 'react';
import { Operator } from '../../../constant'; import { Operator, SingleOperators } from '../../../constant';
import { AgentInstanceContext, HandleContext } from '../../../context'; import { AgentInstanceContext, HandleContext } from '../../../context';
import OperatorIcon from '../../../operator-icon'; import OperatorIcon from '../../../operator-icon';
@ -110,6 +112,20 @@ function OperatorItemList({
return <ul className="space-y-2">{operators.map(renderOperatorItem)}</ul>; return <ul className="space-y-2">{operators.map(renderOperatorItem)}</ul>;
} }
// Limit the number of operators of a certain type on the canvas to only one
function useRestrictSingleOperatorOnCanvas() {
const list: Operator[] = [];
const { findNodeByName } = useGraphStore((state) => state);
SingleOperators.forEach((operator) => {
if (!findNodeByName(operator)) {
list.push(operator);
}
});
return list;
}
function AccordionOperators({ function AccordionOperators({
isCustomDropdown = false, isCustomDropdown = false,
mousePosition, mousePosition,
@ -117,15 +133,16 @@ function AccordionOperators({
isCustomDropdown?: boolean; isCustomDropdown?: boolean;
mousePosition?: { x: number; y: number }; mousePosition?: { x: number; y: number };
}) { }) {
const singleOperators = useRestrictSingleOperatorOnCanvas();
const operators = useMemo(() => {
const list = [...singleOperators];
list.push(Operator.Extractor);
return list;
}, [singleOperators]);
return ( return (
<OperatorItemList <OperatorItemList
operators={[ operators={operators}
Operator.Parser,
Operator.Tokenizer,
Operator.Splitter,
Operator.HierarchicalMerger,
Operator.Extractor,
]}
isCustomDropdown={isCustomDropdown} isCustomDropdown={isCustomDropdown}
mousePosition={mousePosition} mousePosition={mousePosition}
></OperatorItemList> ></OperatorItemList>

View File

@ -0,0 +1 @@
export { RagNode as ExtractorNode } from './index';

View File

@ -4,8 +4,9 @@ import { Handle, HandleProps } from '@xyflow/react';
import { Plus } from 'lucide-react'; import { Plus } from 'lucide-react';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { HandleContext } from '../../context'; import { HandleContext } from '../../context';
import useGraphStore from '../../store';
import { useDropdownManager } from '../context'; import { useDropdownManager } from '../context';
import { InnerNextStepDropdown } from './dropdown/next-step-dropdown'; import { NextStepDropdown } from './dropdown/next-step-dropdown';
export function CommonHandle({ export function CommonHandle({
className, className,
@ -17,6 +18,8 @@ export function CommonHandle({
const { canShowDropdown, setActiveDropdown, clearActiveDropdown } = const { canShowDropdown, setActiveDropdown, clearActiveDropdown } =
useDropdownManager(); useDropdownManager();
const { hasChildNode } = useGraphStore((state) => state);
const value = useMemo( const value = useMemo(
() => ({ () => ({
nodeId, nodeId,
@ -39,6 +42,10 @@ export function CommonHandle({
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
if (hasChildNode(nodeId)) {
return;
}
if (!canShowDropdown()) { if (!canShowDropdown()) {
return; return;
} }
@ -49,14 +56,14 @@ export function CommonHandle({
> >
<Plus className="size-3 pointer-events-none text-text-title-invert" /> <Plus className="size-3 pointer-events-none text-text-title-invert" />
{visible && ( {visible && (
<InnerNextStepDropdown <NextStepDropdown
hideModal={() => { hideModal={() => {
hideModal(); hideModal();
clearActiveDropdown(); clearActiveDropdown();
}} }}
> >
<span></span> <span></span>
</InnerNextStepDropdown> </NextStepDropdown>
)} )}
</Handle> </Handle>
</HandleContext.Provider> </HandleContext.Provider>

View File

@ -1,7 +1,8 @@
import { IRagNode } from '@/interfaces/database/flow'; import { IRagNode } from '@/interfaces/database/flow';
import { NodeProps, Position } from '@xyflow/react'; import { NodeProps, Position } from '@xyflow/react';
import { memo } from 'react'; import { memo, useMemo } from 'react';
import { NodeHandleId } from '../../constant'; import { NodeHandleId, SingleOperators } from '../../constant';
import useGraphStore from '../../store';
import { CommonHandle } from './handle'; import { CommonHandle } from './handle';
import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
import NodeHeader from './node-header'; import NodeHeader from './node-header';
@ -14,8 +15,17 @@ function InnerRagNode({
isConnectable = true, isConnectable = true,
selected, selected,
}: NodeProps<IRagNode>) { }: NodeProps<IRagNode>) {
const getOperatorTypeFromId = useGraphStore(
(state) => state.getOperatorTypeFromId,
);
const showCopy = useMemo(() => {
const operatorName = getOperatorTypeFromId(id);
return SingleOperators.every((x) => x !== operatorName);
}, [getOperatorTypeFromId, id]);
return ( return (
<ToolBar selected={selected} id={id} label={data.label}> <ToolBar selected={selected} id={id} label={data.label} showCopy={showCopy}>
<NodeWrapper selected={selected}> <NodeWrapper selected={selected}>
<CommonHandle <CommonHandle
id={NodeHandleId.End} id={NodeHandleId.End}

View File

@ -1,41 +0,0 @@
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<ILogicNode>) {
return (
<ToolBar selected={selected} id={id} label={data.label}>
<NodeWrapper selected={selected}>
<CommonHandle
id="c"
type="source"
position={Position.Left}
isConnectable={isConnectable}
style={LeftHandleStyle}
nodeId={id}
></CommonHandle>
<CommonHandle
type="source"
position={Position.Right}
isConnectable={isConnectable}
style={RightHandleStyle}
id="b"
nodeId={id}
></CommonHandle>
<NodeHeader id={id} name={data.name} label={data.label}></NodeHeader>
</NodeWrapper>
</ToolBar>
);
}
export const LogicNode = memo(InnerLogicNode);

View File

@ -2,12 +2,10 @@ import { IRagNode } from '@/interfaces/database/flow';
import { NodeProps, Position } from '@xyflow/react'; import { NodeProps, Position } from '@xyflow/react';
import { memo } from 'react'; import { memo } from 'react';
import { NodeHandleId } from '../../constant'; import { NodeHandleId } from '../../constant';
import { needsSingleStepDebugging } from '../../utils';
import { CommonHandle } from './handle'; import { CommonHandle } from './handle';
import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
import NodeHeader from './node-header'; import NodeHeader from './node-header';
import { NodeWrapper } from './node-wrapper'; import { NodeWrapper } from './node-wrapper';
import { ToolBar } from './toolbar';
function ParserNode({ function ParserNode({
id, id,
@ -16,33 +14,26 @@ function ParserNode({
selected, selected,
}: NodeProps<IRagNode>) { }: NodeProps<IRagNode>) {
return ( return (
<ToolBar <NodeWrapper selected={selected}>
selected={selected} <CommonHandle
id={id} id={NodeHandleId.End}
label={data.label} type="target"
showRun={needsSingleStepDebugging(data.label)} position={Position.Left}
> isConnectable={isConnectable}
<NodeWrapper selected={selected}> style={LeftHandleStyle}
<CommonHandle nodeId={id}
id={NodeHandleId.End} ></CommonHandle>
type="target" <CommonHandle
position={Position.Left} type="source"
isConnectable={isConnectable} position={Position.Right}
style={LeftHandleStyle} isConnectable={isConnectable}
nodeId={id} id={NodeHandleId.Start}
></CommonHandle> style={RightHandleStyle}
<CommonHandle nodeId={id}
type="source" isConnectableEnd={false}
position={Position.Right} ></CommonHandle>
isConnectable={isConnectable} <NodeHeader id={id} name={data.name} label={data.label}></NodeHeader>
id={NodeHandleId.Start} </NodeWrapper>
style={RightHandleStyle}
nodeId={id}
isConnectableEnd={false}
></CommonHandle>
<NodeHeader id={id} name={data.name} label={data.label}></NodeHeader>
</NodeWrapper>
</ToolBar>
); );
} }

View File

@ -2,9 +2,8 @@ import { IRagNode } from '@/interfaces/database/flow';
import { NodeProps, Position } from '@xyflow/react'; import { NodeProps, Position } from '@xyflow/react';
import { memo } from 'react'; import { memo } from 'react';
import { NodeHandleId } from '../../constant'; import { NodeHandleId } from '../../constant';
import { needsSingleStepDebugging } from '../../utils';
import { CommonHandle } from './handle'; import { CommonHandle } from './handle';
import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; import { LeftHandleStyle } from './handle-icon';
import NodeHeader from './node-header'; import NodeHeader from './node-header';
import { NodeWrapper } from './node-wrapper'; import { NodeWrapper } from './node-wrapper';
import { ToolBar } from './toolbar'; import { ToolBar } from './toolbar';
@ -20,7 +19,8 @@ function TokenizerNode({
selected={selected} selected={selected}
id={id} id={id}
label={data.label} label={data.label}
showRun={needsSingleStepDebugging(data.label)} showRun={false}
showCopy={false}
> >
<NodeWrapper selected={selected}> <NodeWrapper selected={selected}>
<CommonHandle <CommonHandle
@ -31,15 +31,6 @@ function TokenizerNode({
style={LeftHandleStyle} style={LeftHandleStyle}
nodeId={id} nodeId={id}
></CommonHandle> ></CommonHandle>
<CommonHandle
type="source"
position={Position.Right}
isConnectable={isConnectable}
id={NodeHandleId.Start}
style={RightHandleStyle}
nodeId={id}
isConnectableEnd={false}
></CommonHandle>
<NodeHeader id={id} name={data.name} label={data.label}></NodeHeader> <NodeHeader id={id} name={data.name} label={data.label}></NodeHeader>
</NodeWrapper> </NodeWrapper>
</ToolBar> </ToolBar>

View File

@ -27,6 +27,7 @@ type ToolBarProps = {
label: string; label: string;
id: string; id: string;
showRun?: boolean; showRun?: boolean;
showCopy?: boolean;
} & PropsWithChildren; } & PropsWithChildren;
export function ToolBar({ export function ToolBar({
@ -35,6 +36,7 @@ export function ToolBar({
label, label,
id, id,
showRun = false, showRun = false,
showCopy = true,
}: ToolBarProps) { }: ToolBarProps) {
const deleteNodeById = useGraphStore((store) => store.deleteNodeById); const deleteNodeById = useGraphStore((store) => store.deleteNodeById);
@ -66,10 +68,13 @@ export function ToolBar({
<IconWrapper> <IconWrapper>
<Play className="size-3.5" data-play /> <Play className="size-3.5" data-play />
</IconWrapper> </IconWrapper>
)}{' '} )}
<IconWrapper onClick={handleDuplicate}> {showCopy && (
<Copy className="size-3.5" /> <IconWrapper onClick={handleDuplicate}>
</IconWrapper> <Copy className="size-3.5" />
</IconWrapper>
)}
<IconWrapper onClick={deleteNode}> <IconWrapper onClick={deleteNode}>
<Trash2 className="size-3.5" /> <Trash2 className="size-3.5" />
</IconWrapper> </IconWrapper>

View File

@ -325,13 +325,14 @@ export const CategorizeAnchorPointPositions = [
// key is the source of the edge, value is the target of the edge // key is the source of the edge, value is the target of the edge
// no connection lines are allowed between key and value // no connection lines are allowed between key and value
export const RestrictedUpstreamMap = { export const RestrictedUpstreamMap: Record<Operator, Operator[]> = {
[Operator.Begin]: [], [Operator.Begin]: [] as Operator[],
[Operator.Parser]: [Operator.Begin], [Operator.Parser]: [Operator.Begin],
[Operator.Splitter]: [Operator.Begin], [Operator.Splitter]: [Operator.Begin],
[Operator.HierarchicalMerger]: [Operator.Begin], [Operator.HierarchicalMerger]: [Operator.Begin],
[Operator.Tokenizer]: [Operator.Begin], [Operator.Tokenizer]: [Operator.Begin],
[Operator.Extractor]: [Operator.Begin], [Operator.Extractor]: [Operator.Begin],
[Operator.Note]: [Operator.Begin],
}; };
export const NodeMap = { export const NodeMap = {
@ -411,3 +412,10 @@ export const FileTypeSuffixMap = {
'ape', 'ape',
], ],
}; };
export const SingleOperators = [
Operator.Tokenizer,
Operator.Splitter,
Operator.HierarchicalMerger,
Operator.Parser,
];

View File

@ -14,7 +14,6 @@ import {
applyEdgeChanges, applyEdgeChanges,
applyNodeChanges, applyNodeChanges,
} from '@xyflow/react'; } from '@xyflow/react';
import { omit } from 'lodash';
import differenceWith from 'lodash/differenceWith'; import differenceWith from 'lodash/differenceWith';
import intersectionWith from 'lodash/intersectionWith'; import intersectionWith from 'lodash/intersectionWith';
import lodashSet from 'lodash/set'; import lodashSet from 'lodash/set';
@ -59,7 +58,6 @@ export type RFState = {
updateNode: (node: RAGFlowNodeType) => void; updateNode: (node: RAGFlowNodeType) => void;
addEdge: (connection: Connection) => void; addEdge: (connection: Connection) => void;
getEdge: (id: string) => Edge | undefined; getEdge: (id: string) => Edge | undefined;
updateFormDataOnConnect: (connection: Connection) => void;
updateSwitchFormData: ( updateSwitchFormData: (
source: string, source: string,
sourceHandle?: string | null, sourceHandle?: string | null,
@ -67,7 +65,6 @@ export type RFState = {
isConnecting?: boolean, isConnecting?: boolean,
) => void; ) => void;
duplicateNode: (id: string, name: string) => void; duplicateNode: (id: string, name: string) => void;
duplicateIterationNode: (id: string, name: string) => void;
deleteEdge: () => void; deleteEdge: () => void;
deleteEdgeById: (id: string) => void; deleteEdgeById: (id: string) => void;
deleteNodeById: (id: string) => void; deleteNodeById: (id: string) => void;
@ -89,6 +86,7 @@ export type RFState = {
) => void; // Deleting a condition of a classification operator will delete the related edge ) => void; // Deleting a condition of a classification operator will delete the related edge
findAgentToolNodeById: (id: string | null) => string | undefined; findAgentToolNodeById: (id: string | null) => string | undefined;
selectNodeIds: (nodeIds: string[]) => void; selectNodeIds: (nodeIds: string[]) => void;
hasChildNode: (nodeId: string) => boolean;
}; };
// this is our useStore hook that we can use in our components to get parts of the store and call actions // this is our useStore hook that we can use in our components to get parts of the store and call actions
@ -126,11 +124,9 @@ const useGraphStore = create<RFState>()(
setEdges(mapEdgeMouseEvent(edges, edgeId, false)); setEdges(mapEdgeMouseEvent(edges, edgeId, false));
}, },
onConnect: (connection: Connection) => { onConnect: (connection: Connection) => {
const { updateFormDataOnConnect } = get();
set({ set({
edges: addEdge(connection, get().edges), edges: addEdge(connection, get().edges),
}); });
updateFormDataOnConnect(connection);
}, },
onSelectionChange: ({ nodes, edges }: OnSelectionChangeParams) => { onSelectionChange: ({ nodes, edges }: OnSelectionChangeParams) => {
set({ set({
@ -217,37 +213,14 @@ const useGraphStore = create<RFState>()(
set({ set({
edges: addEdge(connection, get().edges), edges: addEdge(connection, get().edges),
}); });
// TODO: This may not be reasonable. You need to choose between listening to changes in the form.
get().updateFormDataOnConnect(connection);
}, },
getEdge: (id: string) => { getEdge: (id: string) => {
return get().edges.find((x) => x.id === id); return get().edges.find((x) => x.id === id);
}, },
updateFormDataOnConnect: (connection: Connection) => {
const { getOperatorTypeFromId, updateSwitchFormData } = get();
const { source, target, sourceHandle } = connection;
const operatorType = getOperatorTypeFromId(source);
if (source) {
switch (operatorType) {
case Operator.Switch: {
updateSwitchFormData(source, sourceHandle, target, true);
break;
}
default:
break;
}
}
},
duplicateNode: (id: string, name: string) => { duplicateNode: (id: string, name: string) => {
const { getNode, addNode, generateNodeName, duplicateIterationNode } = const { getNode, addNode, generateNodeName } = get();
get();
const node = getNode(id); const node = getNode(id);
if (node?.data.label === Operator.Iteration) {
duplicateIterationNode(id, name);
return;
}
addNode({ addNode({
...(node || {}), ...(node || {}),
data: { data: {
@ -257,35 +230,6 @@ const useGraphStore = create<RFState>()(
...generateDuplicateNode(node?.position, node?.data?.label), ...generateDuplicateNode(node?.position, node?.data?.label),
}); });
}, },
duplicateIterationNode: (id: string, name: string) => {
const { getNode, generateNodeName, nodes } = get();
const node = getNode(id);
const iterationNode: RAGFlowNodeType = {
...(node || {}),
data: {
...(node?.data || { label: Operator.Iteration, form: {} }),
name: generateNodeName(name),
},
...generateDuplicateNode(node?.position, node?.data?.label),
};
const children = nodes
.filter((x) => x.parentId === node?.id)
.map((x) => ({
...(x || {}),
data: {
...duplicateNodeForm(x?.data),
name: generateNodeName(x.data.name),
},
...omit(generateDuplicateNode(x?.position, x?.data?.label), [
'position',
]),
parentId: iterationNode.id,
}));
set({ nodes: nodes.concat(iterationNode, ...children) });
},
deleteEdge: () => { deleteEdge: () => {
const { edges, selectedEdgeIds } = get(); const { edges, selectedEdgeIds } = get();
set({ set({
@ -295,55 +239,15 @@ const useGraphStore = create<RFState>()(
}); });
}, },
deleteEdgeById: (id: string) => { deleteEdgeById: (id: string) => {
const { const { edges } = get();
edges,
updateNodeForm,
getOperatorTypeFromId,
updateSwitchFormData,
} = get();
const currentEdge = edges.find((x) => x.id === id);
if (currentEdge) {
const { source, sourceHandle, target } = currentEdge;
const operatorType = getOperatorTypeFromId(source);
// After deleting the edge, set the corresponding field in the node's form field to undefined
switch (operatorType) {
case Operator.Relevant:
updateNodeForm(source, {
[sourceHandle as string]: undefined,
});
break;
// case Operator.Categorize:
// if (sourceHandle)
// updateNodeForm(source, undefined, [
// 'category_description',
// sourceHandle,
// 'to',
// ]);
// break;
case Operator.Switch: {
updateSwitchFormData(source, sourceHandle, target, false);
break;
}
default:
break;
}
}
set({ set({
edges: edges.filter((edge) => edge.id !== id), edges: edges.filter((edge) => edge.id !== id),
}); });
}, },
deleteNodeById: (id: string) => { deleteNodeById: (id: string) => {
const { const { nodes, edges } = get();
nodes,
edges,
getOperatorTypeFromId,
deleteAgentDownstreamNodesById,
} = get();
if (getOperatorTypeFromId(id) === Operator.Agent) {
deleteAgentDownstreamNodesById(id);
return;
}
set({ set({
nodes: nodes.filter((node) => node.id !== id), nodes: nodes.filter((node) => node.id !== id),
edges: edges edges: edges
@ -526,6 +430,10 @@ const useGraphStore = create<RFState>()(
})), })),
); );
}, },
hasChildNode: (nodeId) => {
const { edges } = get();
return edges.some((edge) => edge.source === nodeId);
},
})), })),
{ name: 'graph', trace: true }, { name: 'graph', trace: true },
), ),