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 ; } +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 }, ),