mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-02-05 18:15:06 +08:00
Compare commits
2 Commits
8bc8126848
...
c49e81882c
| Author | SHA1 | Date | |
|---|---|---|---|
| c49e81882c | |||
| 63cdce660e |
@ -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',
|
||||||
|
|||||||
@ -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: '提示词将发生变化,请确认是否放弃已有提示词?',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
export { RagNode as ContextNode } from './index';
|
|
||||||
@ -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>
|
||||||
|
|||||||
1
web/src/pages/data-flow/canvas/node/extractor-node.tsx
Normal file
1
web/src/pages/data-flow/canvas/node/extractor-node.tsx
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { RagNode as ExtractorNode } from './index';
|
||||||
@ -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>
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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);
|
|
||||||
@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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,
|
||||||
|
];
|
||||||
|
|||||||
@ -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 },
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user