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)
This commit is contained in:
balibabu
2025-09-29 18:55:53 +08:00
committed by GitHub
parent 63cdce660e
commit c49e81882c
11 changed files with 73 additions and 123 deletions

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 {};
}, [source, target, graphPath]); }
return {};
}, [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

@ -42,8 +42,8 @@ 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 { NextStepDropdown } 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';
@ -58,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 = {
@ -316,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

@ -24,7 +24,7 @@ import {
useMemo, 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';
@ -112,18 +112,12 @@ function OperatorItemList({
return <ul className="space-y-2">{operators.map(renderOperatorItem)}</ul>; return <ul className="space-y-2">{operators.map(renderOperatorItem)}</ul>;
} }
const singleOperators = [
Operator.Tokenizer,
Operator.Splitter,
Operator.HierarchicalMerger,
Operator.Parser,
];
// Limit the number of operators of a certain type on the canvas to only one // Limit the number of operators of a certain type on the canvas to only one
function useRestrictSingleOperatorOnCanvas() { function useRestrictSingleOperatorOnCanvas() {
const list: Operator[] = []; const list: Operator[] = [];
const { findNodeByName } = useGraphStore((state) => state); const { findNodeByName } = useGraphStore((state) => state);
singleOperators.forEach((operator) => { SingleOperators.forEach((operator) => {
if (!findNodeByName(operator)) { if (!findNodeByName(operator)) {
list.push(operator); list.push(operator);
} }

View File

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

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,12 +14,6 @@ function ParserNode({
selected, selected,
}: NodeProps<IRagNode>) { }: NodeProps<IRagNode>) {
return ( return (
<ToolBar
selected={selected}
id={id}
label={data.label}
showRun={needsSingleStepDebugging(data.label)}
>
<NodeWrapper selected={selected}> <NodeWrapper selected={selected}>
<CommonHandle <CommonHandle
id={NodeHandleId.End} id={NodeHandleId.End}
@ -42,7 +34,6 @@ function ParserNode({
></CommonHandle> ></CommonHandle>
<NodeHeader id={id} name={data.name} label={data.label}></NodeHeader> <NodeHeader id={id} name={data.name} label={data.label}></NodeHeader>
</NodeWrapper> </NodeWrapper>
</ToolBar>
); );
} }

View File

@ -2,7 +2,6 @@ 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 } from './handle-icon'; import { LeftHandleStyle } from './handle-icon';
import NodeHeader from './node-header'; import NodeHeader from './node-header';
@ -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

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>
)}{' '} )}
{showCopy && (
<IconWrapper onClick={handleDuplicate}> <IconWrapper onClick={handleDuplicate}>
<Copy className="size-3.5" /> <Copy className="size-3.5" />
</IconWrapper> </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,
];