diff --git a/web/src/locales/en.ts b/web/src/locales/en.ts
index 63769fb60..007bb72df 100644
--- a/web/src/locales/en.ts
+++ b/web/src/locales/en.ts
@@ -1687,15 +1687,20 @@ This delimiter is used to split the input text into several text pieces echo of
},
dataflow: {
parser: 'Parser',
- parserDescription: 'Parser',
- chunker: 'Chunker',
- chunkerDescription: 'Chunker',
+ parserDescription:
+ 'Extracts raw text and structure from files for downstream processing.',
tokenizer: 'Tokenizer',
- tokenizerDescription: 'Tokenizer',
- splitter: 'Splitter',
- splitterDescription: 'Splitter',
- hierarchicalMergerDescription: 'Hierarchical merger',
- hierarchicalMerger: 'Hierarchical merger',
+ tokenizerDescription:
+ 'Transforms text into the required data structure (e.g., vector embeddings for Embedding Search) depending on the chosen search method.',
+ splitter: 'Token Splitter',
+ splitterDescription:
+ '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',
lang: 'Language',
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',
viewResult: 'View Result',
running: 'Running',
- extractor: 'Extractor',
- extractorDescription: 'Extractor',
summary: 'Augmented Context',
keywords: 'Keywords',
questions: 'Questions',
diff --git a/web/src/locales/zh.ts b/web/src/locales/zh.ts
index 4dba7b4aa..38595e808 100644
--- a/web/src/locales/zh.ts
+++ b/web/src/locales/zh.ts
@@ -1605,15 +1605,19 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于
},
dataflow: {
parser: '解析器',
- parserDescription: '解析器',
- chunker: '分块器',
- chunkerDescription: '分块器',
+ parserDescription: '从文件中提取原始文本和结构以供下游处理。',
tokenizer: '分词器',
- tokenizerDescription: '分词器',
- splitter: '拆分器',
- splitterDescription: '拆分器',
- hierarchicalMergerDesription: '分层合并',
- hierarchicalMerger: '分层合并',
+ tokenizerDescription:
+ '根据所选的搜索方法,将文本转换为所需的数据结构(例如,用于嵌入搜索的向量嵌入)。',
+ splitter: '分词器拆分器',
+ splitterDescription:
+ '根据分词器长度将文本拆分成块,并带有可选的分隔符和重叠。',
+ hierarchicalMergerDescription:
+ '使用正则表达式规则按标题层次结构将文档拆分成多个部分,以实现更精细的控制。',
+ hierarchicalMerger: '标题拆分器',
+ extractor: '提取器',
+ extractorDescription:
+ '使用 LLM 从文档块(例如摘要、分类等)中提取结构化见解。',
outputFormat: '输出格式',
lang: '语言',
fileFormats: '文件格式',
@@ -1632,8 +1636,6 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于
exportJson: '导出 JSON',
viewResult: '查看结果',
running: '运行中',
- extractor: '提取器',
- extractorDescription: '提取器',
summary: '增强上下文',
keywords: '关键词',
questions: '问题',
@@ -1687,6 +1689,7 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于
},
},
cancel: '取消',
+ filenameEmbeddingWeight: '文件名嵌入权重',
switchPromptMessage: '提示词将发生变化,请确认是否放弃已有提示词?',
},
},
diff --git a/web/src/pages/data-flow/canvas/index.tsx b/web/src/pages/data-flow/canvas/index.tsx
index a079b63c7..e57087377 100644
--- a/web/src/pages/data-flow/canvas/index.tsx
+++ b/web/src/pages/data-flow/canvas/index.tsx
@@ -12,6 +12,7 @@ import {
ControlButton,
Controls,
NodeTypes,
+ OnConnectEnd,
Position,
ReactFlow,
ReactFlowInstance,
@@ -36,12 +37,13 @@ import {
useShowDrawer,
} from '../hooks/use-show-drawer';
import RunSheet from '../run-sheet';
+import useGraphStore from '../store';
import { ButtonEdge } from './edge';
import styles from './index.less';
import { RagNode } from './node';
import { BeginNode } from './node/begin-node';
import { ContextNode } from './node/context-node';
-import { InnerNextStepDropdown } from './node/dropdown/next-step-dropdown';
+import { NextStepDropdown } from './node/dropdown/next-step-dropdown';
import { HierarchicalMergerNode } from './node/hierarchical-merger-node';
import NoteNode from './node/note-node';
import ParserNode from './node/parser-node';
@@ -116,6 +118,7 @@ function DataFlowCanvas({ drawerVisible, hideDrawer, showLogSheet }: IProps) {
useHideFormSheetOnNodeDeletion({ hideFormDrawer });
const { visible, hideModal, showModal } = useSetModalState();
+
const [dropdownPosition, setDropdownPosition] = useState({ x: 0, y: 0 });
const isConnectedRef = useRef(false);
@@ -128,6 +131,8 @@ function DataFlowCanvas({ drawerVisible, hideDrawer, showLogSheet }: IProps) {
const { setActiveDropdown, clearActiveDropdown } = useDropdownManager();
+ const { hasChildNode } = useGraphStore((state) => state);
+
const onPaneClick = useCallback(() => {
hideFormDrawer();
if (visible && !preventCloseRef.current) {
@@ -159,7 +164,7 @@ function DataFlowCanvas({ drawerVisible, hideDrawer, showLogSheet }: IProps) {
isConnectedRef.current = true;
};
- const OnConnectStart = (event: any, params: any) => {
+ const onConnectStart = (event: any, params: any) => {
isConnectedRef.current = false;
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) {
const { clientX, clientY } = event;
setDropdownPosition({ x: clientX, y: clientY });
@@ -220,8 +230,8 @@ function DataFlowCanvas({ drawerVisible, hideDrawer, showLogSheet }: IProps) {
onConnect={onConnect}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
- onConnectStart={OnConnectStart}
- onConnectEnd={OnConnectEnd}
+ onConnectStart={onConnectStart}
+ onConnectEnd={onConnectEnd}
onNodeClick={onNodeClick}
onPaneClick={onPaneClick}
onInit={setReactFlowInstance}
@@ -268,7 +278,7 @@ function DataFlowCanvas({ drawerVisible, hideDrawer, showLogSheet }: IProps) {
isFromConnectionDrag: true,
}}
>
- {
hideModal();
clearActiveDropdown();
@@ -276,7 +286,7 @@ function DataFlowCanvas({ drawerVisible, hideDrawer, showLogSheet }: IProps) {
position={dropdownPosition}
>
-
+
)}
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
index 5ca150836..403980b13 100644
--- 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
@@ -12,6 +12,7 @@ import {
} from '@/components/ui/tooltip';
import { IModalProps } from '@/interfaces/common';
import { useGetNodeDescription, useGetNodeName } from '@/pages/data-flow/hooks';
+import useGraphStore from '@/pages/data-flow/store';
import { Position } from '@xyflow/react';
import { t } from 'i18next';
import {
@@ -20,6 +21,7 @@ import {
memo,
useContext,
useEffect,
+ useMemo,
useRef,
} from 'react';
import { Operator } from '../../../constant';
@@ -110,6 +112,26 @@ function OperatorItemList({
return
{operators.map(renderOperatorItem)}
;
}
+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
+function useRestrictSingleOperatorOnCanvas() {
+ const list: Operator[] = [];
+ const { findNodeByName } = useGraphStore((state) => state);
+
+ singleOperators.forEach((operator) => {
+ if (!findNodeByName(operator)) {
+ list.push(operator);
+ }
+ });
+
+ return list;
+}
+
function AccordionOperators({
isCustomDropdown = false,
mousePosition,
@@ -117,15 +139,16 @@ function AccordionOperators({
isCustomDropdown?: boolean;
mousePosition?: { x: number; y: number };
}) {
+ const singleOperators = useRestrictSingleOperatorOnCanvas();
+ const operators = useMemo(() => {
+ const list = [...singleOperators];
+ list.push(Operator.Extractor);
+ return list;
+ }, [singleOperators]);
+
return (
diff --git a/web/src/pages/data-flow/canvas/node/handle.tsx b/web/src/pages/data-flow/canvas/node/handle.tsx
index 71b473cc7..da61e3b4c 100644
--- a/web/src/pages/data-flow/canvas/node/handle.tsx
+++ b/web/src/pages/data-flow/canvas/node/handle.tsx
@@ -4,8 +4,9 @@ import { Handle, HandleProps } from '@xyflow/react';
import { Plus } from 'lucide-react';
import { useMemo } from 'react';
import { HandleContext } from '../../context';
+import useGraphStore from '../../store';
import { useDropdownManager } from '../context';
-import { InnerNextStepDropdown } from './dropdown/next-step-dropdown';
+import { NextStepDropdown } from './dropdown/next-step-dropdown';
export function CommonHandle({
className,
@@ -17,6 +18,8 @@ export function CommonHandle({
const { canShowDropdown, setActiveDropdown, clearActiveDropdown } =
useDropdownManager();
+ const { hasChildNode } = useGraphStore((state) => state);
+
const value = useMemo(
() => ({
nodeId,
@@ -39,6 +42,10 @@ export function CommonHandle({
onClick={(e) => {
e.stopPropagation();
+ if (hasChildNode(nodeId)) {
+ return;
+ }
+
if (!canShowDropdown()) {
return;
}
@@ -49,14 +56,14 @@ export function CommonHandle({
>
{visible && (
- {
hideModal();
clearActiveDropdown();
}}
>
-
+
)}
diff --git a/web/src/pages/data-flow/canvas/node/tokenizer-node.tsx b/web/src/pages/data-flow/canvas/node/tokenizer-node.tsx
index 141399b14..f6481b86a 100644
--- a/web/src/pages/data-flow/canvas/node/tokenizer-node.tsx
+++ b/web/src/pages/data-flow/canvas/node/tokenizer-node.tsx
@@ -4,7 +4,7 @@ import { memo } from 'react';
import { NodeHandleId } from '../../constant';
import { needsSingleStepDebugging } from '../../utils';
import { CommonHandle } from './handle';
-import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
+import { LeftHandleStyle } from './handle-icon';
import NodeHeader from './node-header';
import { NodeWrapper } from './node-wrapper';
import { ToolBar } from './toolbar';
@@ -31,15 +31,6 @@ function TokenizerNode({
style={LeftHandleStyle}
nodeId={id}
>
-
diff --git a/web/src/pages/data-flow/store.ts b/web/src/pages/data-flow/store.ts
index e163ff8cb..64fda97a4 100644
--- a/web/src/pages/data-flow/store.ts
+++ b/web/src/pages/data-flow/store.ts
@@ -14,7 +14,6 @@ import {
applyEdgeChanges,
applyNodeChanges,
} from '@xyflow/react';
-import { omit } from 'lodash';
import differenceWith from 'lodash/differenceWith';
import intersectionWith from 'lodash/intersectionWith';
import lodashSet from 'lodash/set';
@@ -59,7 +58,6 @@ export type RFState = {
updateNode: (node: RAGFlowNodeType) => void;
addEdge: (connection: Connection) => void;
getEdge: (id: string) => Edge | undefined;
- updateFormDataOnConnect: (connection: Connection) => void;
updateSwitchFormData: (
source: string,
sourceHandle?: string | null,
@@ -67,7 +65,6 @@ export type RFState = {
isConnecting?: boolean,
) => void;
duplicateNode: (id: string, name: string) => void;
- duplicateIterationNode: (id: string, name: string) => void;
deleteEdge: () => void;
deleteEdgeById: (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
findAgentToolNodeById: (id: string | null) => string | undefined;
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
@@ -126,11 +124,9 @@ const useGraphStore = create()(
setEdges(mapEdgeMouseEvent(edges, edgeId, false));
},
onConnect: (connection: Connection) => {
- const { updateFormDataOnConnect } = get();
set({
edges: addEdge(connection, get().edges),
});
- updateFormDataOnConnect(connection);
},
onSelectionChange: ({ nodes, edges }: OnSelectionChangeParams) => {
set({
@@ -217,37 +213,14 @@ const useGraphStore = create()(
set({
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) => {
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) => {
- const { getNode, addNode, generateNodeName, duplicateIterationNode } =
- get();
+ const { getNode, addNode, generateNodeName } = get();
const node = getNode(id);
- if (node?.data.label === Operator.Iteration) {
- duplicateIterationNode(id, name);
- return;
- }
-
addNode({
...(node || {}),
data: {
@@ -257,35 +230,6 @@ const useGraphStore = create()(
...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: () => {
const { edges, selectedEdgeIds } = get();
set({
@@ -295,55 +239,15 @@ const useGraphStore = create()(
});
},
deleteEdgeById: (id: string) => {
- const {
- edges,
- updateNodeForm,
- getOperatorTypeFromId,
- updateSwitchFormData,
- } = get();
- const currentEdge = edges.find((x) => x.id === id);
+ const { edges } = get();
- 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({
edges: edges.filter((edge) => edge.id !== id),
});
},
deleteNodeById: (id: string) => {
- const {
- nodes,
- edges,
- getOperatorTypeFromId,
- deleteAgentDownstreamNodesById,
- } = get();
- if (getOperatorTypeFromId(id) === Operator.Agent) {
- deleteAgentDownstreamNodesById(id);
- return;
- }
+ const { nodes, edges } = get();
+
set({
nodes: nodes.filter((node) => node.id !== id),
edges: edges
@@ -526,6 +430,10 @@ const useGraphStore = create()(
})),
);
},
+ hasChildNode: (nodeId) => {
+ const { edges } = get();
+ return edges.some((edge) => edge.source === nodeId);
+ },
})),
{ name: 'graph', trace: true },
),