Feat: Initialize the data pipeline canvas. #9869 (#9870)

### What problem does this PR solve?
Feat: Initialize the data pipeline canvas. #9869

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu
2025-09-02 15:47:33 +08:00
committed by GitHub
parent c2567844ea
commit cb14dafaca
196 changed files with 21201 additions and 0 deletions

View File

@ -0,0 +1,462 @@
import { useFetchModelId } from '@/hooks/logic-hooks';
import { Connection, Node, Position, ReactFlowInstance } from '@xyflow/react';
import humanId from 'human-id';
import { t } from 'i18next';
import { lowerFirst } from 'lodash';
import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import {
NodeHandleId,
NodeMap,
Operator,
initialAgentValues,
initialAkShareValues,
initialArXivValues,
initialBaiduFanyiValues,
initialBaiduValues,
initialBeginValues,
initialBingValues,
initialCategorizeValues,
initialCodeValues,
initialConcentratorValues,
initialCrawlerValues,
initialDeepLValues,
initialDuckValues,
initialEmailValues,
initialExeSqlValues,
initialGithubValues,
initialGoogleScholarValues,
initialGoogleValues,
initialInvokeValues,
initialIterationStartValues,
initialIterationValues,
initialJin10Values,
initialKeywordExtractValues,
initialMessageValues,
initialNoteValues,
initialPubMedValues,
initialQWeatherValues,
initialRelevantValues,
initialRetrievalValues,
initialRewriteQuestionValues,
initialSearXNGValues,
initialStringTransformValues,
initialSwitchValues,
initialTavilyExtractValues,
initialTavilyValues,
initialTuShareValues,
initialUserFillUpValues,
initialWaitingDialogueValues,
initialWenCaiValues,
initialWikipediaValues,
initialYahooFinanceValues,
} from '../constant';
import useGraphStore from '../store';
import {
generateNodeNamesWithIncreasingIndex,
getNodeDragHandle,
} from '../utils';
function isBottomSubAgent(type: string, position: Position) {
return (
(type === Operator.Agent && position === Position.Bottom) ||
type === Operator.Tool
);
}
export const useInitializeOperatorParams = () => {
const llmId = useFetchModelId();
const initialFormValuesMap = useMemo(() => {
return {
[Operator.Begin]: initialBeginValues,
[Operator.Retrieval]: initialRetrievalValues,
[Operator.Categorize]: { ...initialCategorizeValues, llm_id: llmId },
[Operator.Relevant]: { ...initialRelevantValues, llm_id: llmId },
[Operator.RewriteQuestion]: {
...initialRewriteQuestionValues,
llm_id: llmId,
},
[Operator.Message]: initialMessageValues,
[Operator.KeywordExtract]: {
...initialKeywordExtractValues,
llm_id: llmId,
},
[Operator.DuckDuckGo]: initialDuckValues,
[Operator.Baidu]: initialBaiduValues,
[Operator.Wikipedia]: initialWikipediaValues,
[Operator.PubMed]: initialPubMedValues,
[Operator.ArXiv]: initialArXivValues,
[Operator.Google]: initialGoogleValues,
[Operator.Bing]: initialBingValues,
[Operator.GoogleScholar]: initialGoogleScholarValues,
[Operator.DeepL]: initialDeepLValues,
[Operator.SearXNG]: initialSearXNGValues,
[Operator.GitHub]: initialGithubValues,
[Operator.BaiduFanyi]: initialBaiduFanyiValues,
[Operator.QWeather]: initialQWeatherValues,
[Operator.ExeSQL]: initialExeSqlValues,
[Operator.Switch]: initialSwitchValues,
[Operator.WenCai]: initialWenCaiValues,
[Operator.AkShare]: initialAkShareValues,
[Operator.YahooFinance]: initialYahooFinanceValues,
[Operator.Jin10]: initialJin10Values,
[Operator.Concentrator]: initialConcentratorValues,
[Operator.TuShare]: initialTuShareValues,
[Operator.Note]: initialNoteValues,
[Operator.Crawler]: initialCrawlerValues,
[Operator.Invoke]: initialInvokeValues,
[Operator.Email]: initialEmailValues,
[Operator.Iteration]: initialIterationValues,
[Operator.IterationStart]: initialIterationStartValues,
[Operator.Code]: initialCodeValues,
[Operator.WaitingDialogue]: initialWaitingDialogueValues,
[Operator.Agent]: { ...initialAgentValues, llm_id: llmId },
[Operator.Tool]: {},
[Operator.TavilySearch]: initialTavilyValues,
[Operator.UserFillUp]: initialUserFillUpValues,
[Operator.StringTransform]: initialStringTransformValues,
[Operator.TavilyExtract]: initialTavilyExtractValues,
};
}, [llmId]);
const initializeOperatorParams = useCallback(
(operatorName: Operator, position: Position) => {
const initialValues = initialFormValuesMap[operatorName];
if (isBottomSubAgent(operatorName, position)) {
return {
...initialValues,
description: t('flow.descriptionMessage'),
user_prompt: t('flow.userPromptDefaultValue'),
};
}
return initialValues;
},
[initialFormValuesMap],
);
return { initializeOperatorParams, initialFormValuesMap };
};
export const useGetNodeName = () => {
const { t } = useTranslation();
return (type: string) => {
const name = t(`flow.${lowerFirst(type)}`);
return name;
};
};
export function useCalculateNewlyChildPosition() {
const getNode = useGraphStore((state) => state.getNode);
const nodes = useGraphStore((state) => state.nodes);
const edges = useGraphStore((state) => state.edges);
const calculateNewlyBackChildPosition = useCallback(
(id?: string, sourceHandle?: string) => {
const parentNode = getNode(id);
// Calculate the coordinates of child nodes to prevent newly added child nodes from covering other child nodes
const allChildNodeIds = edges
.filter((x) => x.source === id && x.sourceHandle === sourceHandle)
.map((x) => x.target);
const yAxises = nodes
.filter((x) => allChildNodeIds.some((y) => y === x.id))
.map((x) => x.position.y);
const maxY = Math.max(...yAxises);
const position = {
y: yAxises.length > 0 ? maxY + 150 : parentNode?.position.y || 0,
x: (parentNode?.position.x || 0) + 300,
};
return position;
},
[edges, getNode, nodes],
);
return { calculateNewlyBackChildPosition };
}
function useAddChildEdge() {
const addEdge = useGraphStore((state) => state.addEdge);
const addChildEdge = useCallback(
(position: Position = Position.Right, edge: Partial<Connection>) => {
if (
position === Position.Right &&
edge.source &&
edge.target &&
edge.sourceHandle
) {
addEdge({
source: edge.source,
target: edge.target,
sourceHandle: edge.sourceHandle,
targetHandle: NodeHandleId.End,
});
}
},
[addEdge],
);
return { addChildEdge };
}
function useAddToolNode() {
const { nodes, edges, addEdge, getNode, addNode } = useGraphStore(
(state) => state,
);
const addToolNode = useCallback(
(newNode: Node<any>, nodeId?: string): boolean => {
const agentNode = getNode(nodeId);
if (agentNode) {
const childToolNodeIds = edges
.filter(
(x) => x.source === nodeId && x.sourceHandle === NodeHandleId.Tool,
)
.map((x) => x.target);
if (
childToolNodeIds.length > 0 &&
nodes.some((x) => x.id === childToolNodeIds[0])
) {
return false;
}
newNode.position = {
x: agentNode.position.x - 82,
y: agentNode.position.y + 140,
};
addNode(newNode);
if (nodeId) {
addEdge({
source: nodeId,
target: newNode.id,
sourceHandle: NodeHandleId.Tool,
targetHandle: NodeHandleId.End,
});
}
return true;
}
return false;
},
[addEdge, addNode, edges, getNode, nodes],
);
return { addToolNode };
}
function useResizeIterationNode() {
const { getNode, nodes, updateNode } = useGraphStore((state) => state);
const resizeIterationNode = useCallback(
(type: string, position: Position, parentId?: string) => {
const parentNode = getNode(parentId);
if (parentNode && !isBottomSubAgent(type, position)) {
const MoveRightDistance = 310;
const childNodeList = nodes.filter((x) => x.parentId === parentId);
const maxX = Math.max(...childNodeList.map((x) => x.position.x));
if (maxX + MoveRightDistance > parentNode.position.x) {
updateNode({
...parentNode,
width: (parentNode.width || 0) + MoveRightDistance,
position: {
x: parentNode.position.x + MoveRightDistance / 2,
y: parentNode.position.y,
},
});
}
}
},
[getNode, nodes, updateNode],
);
return { resizeIterationNode };
}
type CanvasMouseEvent = Pick<
React.MouseEvent<HTMLElement>,
'clientX' | 'clientY'
>;
export function useAddNode(reactFlowInstance?: ReactFlowInstance<any, any>) {
const { edges, nodes, addEdge, addNode, getNode } = useGraphStore(
(state) => state,
);
const getNodeName = useGetNodeName();
const { initializeOperatorParams } = useInitializeOperatorParams();
const { calculateNewlyBackChildPosition } = useCalculateNewlyChildPosition();
const { addChildEdge } = useAddChildEdge();
const { addToolNode } = useAddToolNode();
const { resizeIterationNode } = useResizeIterationNode();
// const [reactFlowInstance, setReactFlowInstance] =
// useState<ReactFlowInstance<any, any>>();
const addCanvasNode = useCallback(
(
type: string,
params: {
nodeId?: string;
position: Position;
id?: string;
isFromConnectionDrag?: boolean;
} = {
position: Position.Right,
},
) =>
(event?: CanvasMouseEvent): string | undefined => {
const nodeId = params.nodeId;
const node = getNode(nodeId);
// reactFlowInstance.project was renamed to reactFlowInstance.screenToFlowPosition
// and you don't need to subtract the reactFlowBounds.left/top anymore
// details: https://@xyflow/react.dev/whats-new/2023-11-10
let position = reactFlowInstance?.screenToFlowPosition({
x: event?.clientX || 0,
y: event?.clientY || 0,
});
if (
params.position === Position.Right &&
type !== Operator.Note &&
!params.isFromConnectionDrag
) {
position = calculateNewlyBackChildPosition(nodeId, params.id);
}
const newNode: Node<any> = {
id: `${type}:${humanId()}`,
type: NodeMap[type as Operator] || 'ragNode',
position: position || {
x: 0,
y: 0,
},
data: {
label: `${type}`,
name: generateNodeNamesWithIncreasingIndex(
getNodeName(type),
nodes,
),
form: initializeOperatorParams(type as Operator, params.position),
},
sourcePosition: Position.Right,
targetPosition: Position.Left,
dragHandle: getNodeDragHandle(type),
};
if (node && node.parentId) {
newNode.parentId = node.parentId;
newNode.extent = 'parent';
const parentNode = getNode(node.parentId);
if (parentNode && !isBottomSubAgent(type, params.position)) {
resizeIterationNode(type, params.position, node.parentId);
}
}
if (type === Operator.Iteration) {
newNode.width = 500;
newNode.height = 250;
const iterationStartNode: Node<any> = {
id: `${Operator.IterationStart}:${humanId()}`,
type: 'iterationStartNode',
position: { x: 50, y: 100 },
// draggable: false,
data: {
label: Operator.IterationStart,
name: Operator.IterationStart,
form: initialIterationStartValues,
},
parentId: newNode.id,
extent: 'parent',
};
addNode(newNode);
addNode(iterationStartNode);
if (nodeId) {
addEdge({
source: nodeId,
target: newNode.id,
sourceHandle: NodeHandleId.Start,
targetHandle: NodeHandleId.End,
});
}
return newNode.id;
} else if (
type === Operator.Agent &&
params.position === Position.Bottom
) {
const agentNode = getNode(nodeId);
if (agentNode) {
// Calculate the coordinates of child nodes to prevent newly added child nodes from covering other child nodes
const allChildAgentNodeIds = edges
.filter(
(x) =>
x.source === nodeId &&
x.sourceHandle === NodeHandleId.AgentBottom,
)
.map((x) => x.target);
const xAxises = nodes
.filter((x) => allChildAgentNodeIds.some((y) => y === x.id))
.map((x) => x.position.x);
const maxX = Math.max(...xAxises);
newNode.position = {
x: xAxises.length > 0 ? maxX + 262 : agentNode.position.x + 82,
y: agentNode.position.y + 140,
};
}
addNode(newNode);
if (nodeId) {
addEdge({
source: nodeId,
target: newNode.id,
sourceHandle: NodeHandleId.AgentBottom,
targetHandle: NodeHandleId.AgentTop,
});
}
return newNode.id;
} else if (type === Operator.Tool) {
const toolNodeAdded = addToolNode(newNode, params.nodeId);
return toolNodeAdded ? newNode.id : undefined;
} else {
addNode(newNode);
addChildEdge(params.position, {
source: params.nodeId,
target: newNode.id,
sourceHandle: params.id,
});
}
return newNode.id;
},
[
addChildEdge,
addEdge,
addNode,
addToolNode,
calculateNewlyBackChildPosition,
edges,
getNode,
getNodeName,
initializeOperatorParams,
nodes,
reactFlowInstance,
resizeIterationNode,
],
);
const addNoteNode = useCallback(
(e: CanvasMouseEvent) => {
addCanvasNode(Operator.Note)(e);
},
[addCanvasNode],
);
return { addCanvasNode, addNoteNode };
}

View File

@ -0,0 +1,70 @@
import { omit, pick } from 'lodash';
import { useCallback } from 'react';
import { Operator } from '../constant';
import { useInitializeOperatorParams } from './use-add-node';
export function useAgentToolInitialValues() {
const { initialFormValuesMap } = useInitializeOperatorParams();
const initializeAgentToolValues = useCallback(
(operatorName: Operator) => {
const initialValues = initialFormValuesMap[operatorName];
switch (operatorName) {
case Operator.Retrieval:
return {
...omit(initialValues, 'query'),
description: '',
};
case (Operator.TavilySearch, Operator.TavilyExtract):
return {
api_key: '',
};
case Operator.ExeSQL:
return omit(initialValues, 'sql');
case Operator.Bing:
return omit(initialValues, 'query');
case Operator.YahooFinance:
return omit(initialValues, 'stock_code');
case Operator.Email:
return pick(
initialValues,
'smtp_server',
'smtp_port',
'email',
'password',
'sender_name',
);
case Operator.DuckDuckGo:
return pick(initialValues, 'top_n', 'channel');
case Operator.Wikipedia:
return pick(initialValues, 'top_n', 'language');
case Operator.Google:
return pick(initialValues, 'api_key', 'country', 'language');
case Operator.GoogleScholar:
return omit(initialValues, 'query', 'outputs');
case Operator.ArXiv:
return pick(initialValues, 'top_n', 'sort_by');
case Operator.PubMed:
return pick(initialValues, 'top_n', 'email');
case Operator.GitHub:
return pick(initialValues, 'top_n');
case Operator.WenCai:
return pick(initialValues, 'top_n', 'query_type');
case Operator.Code:
return {};
case Operator.SearXNG:
return pick(initialValues, 'searxng_url', 'top_n');
default:
return initialValues;
}
},
[initialFormValuesMap],
);
return { initializeAgentToolValues };
}

View File

@ -0,0 +1,82 @@
import { RAGFlowNodeType } from '@/interfaces/database/flow';
import { Node, OnBeforeDelete } from '@xyflow/react';
import { Operator } from '../constant';
import useGraphStore from '../store';
import { deleteAllDownstreamAgentsAndTool } from '../utils/delete-node';
const UndeletableNodes = [Operator.Begin, Operator.IterationStart];
export function useBeforeDelete() {
const { getOperatorTypeFromId, getNode } = useGraphStore((state) => state);
const agentPredicate = (node: Node) => {
return getOperatorTypeFromId(node.id) === Operator.Agent;
};
const handleBeforeDelete: OnBeforeDelete<RAGFlowNodeType> = async ({
nodes, // Nodes to be deleted
edges, // Edges to be deleted
}) => {
const toBeDeletedNodes = nodes.filter((node) => {
const operatorType = node.data?.label as Operator;
if (operatorType === Operator.Begin) {
return false;
}
if (
operatorType === Operator.IterationStart &&
!nodes.some((x) => x.id === node.parentId)
) {
return false;
}
return true;
});
const toBeDeletedEdges = edges.filter((edge) => {
const sourceType = getOperatorTypeFromId(edge.source) as Operator;
const downStreamNodes = nodes.filter((x) => x.id === edge.target);
// This edge does not need to be deleted, the range of edges that do not need to be deleted is smaller, so consider the case where it does not need to be deleted
if (
UndeletableNodes.includes(sourceType) && // Upstream node is Begin or IterationStart
downStreamNodes.length === 0 // Downstream node does not exist in the nodes to be deleted
) {
if (!nodes.some((x) => x.id === edge.source)) {
return true; // Can be deleted
}
return false; // Cannot be deleted
}
return true;
});
// Delete the agent and tool nodes downstream of the agent node
if (nodes.some(agentPredicate)) {
nodes.filter(agentPredicate).forEach((node) => {
const { downstreamAgentAndToolEdges, downstreamAgentAndToolNodeIds } =
deleteAllDownstreamAgentsAndTool(node.id, edges);
downstreamAgentAndToolNodeIds.forEach((nodeId) => {
const currentNode = getNode(nodeId);
if (toBeDeletedNodes.every((x) => x.id !== nodeId) && currentNode) {
toBeDeletedNodes.push(currentNode);
}
});
downstreamAgentAndToolEdges.forEach((edge) => {
if (toBeDeletedEdges.every((x) => x.id !== edge.id)) {
toBeDeletedEdges.push(edge);
}
});
}, []);
}
return {
nodes: toBeDeletedNodes,
edges: toBeDeletedEdges,
};
};
return { handleBeforeDelete };
}

View File

@ -0,0 +1,29 @@
import { useFetchAgent } from '@/hooks/use-agent-request';
import { RAGFlowNodeType } from '@/interfaces/database/flow';
import { useCallback } from 'react';
import useGraphStore from '../store';
import { buildDslComponentsByGraph } from '../utils';
export const useBuildDslData = () => {
const { data } = useFetchAgent();
const { nodes, edges } = useGraphStore((state) => state);
const buildDslData = useCallback(
(currentNodes?: RAGFlowNodeType[]) => {
const dslComponents = buildDslComponentsByGraph(
currentNodes ?? nodes,
edges,
data.dsl.components,
);
return {
...data.dsl,
graph: { nodes: currentNodes ?? nodes, edges },
components: dslComponents,
};
},
[data.dsl, edges, nodes],
);
return { buildDslData };
};

View File

@ -0,0 +1,88 @@
import {
IEventList,
INodeEvent,
MessageEventType,
} from '@/hooks/use-send-message';
import { useCallback, useEffect, useMemo, useState } from 'react';
export const ExcludeTypes = [
MessageEventType.Message,
MessageEventType.MessageEnd,
];
export function useCacheChatLog() {
const [eventList, setEventList] = useState<IEventList>([]);
const [messageIdPool, setMessageIdPool] = useState<
Record<string, IEventList>
>({});
const [currentMessageId, setCurrentMessageId] = useState('');
useEffect(() => {
setMessageIdPool((prev) => ({ ...prev, [currentMessageId]: eventList }));
}, [currentMessageId, eventList]);
const filterEventListByMessageId = useCallback(
(messageId: string) => {
return messageIdPool[messageId]?.filter(
(x) => x.message_id === messageId,
);
},
[messageIdPool],
);
const filterEventListByEventType = useCallback(
(eventType: string) => {
return messageIdPool[currentMessageId]?.filter(
(x) => x.event === eventType,
);
},
[messageIdPool, currentMessageId],
);
const clearEventList = useCallback(() => {
setEventList([]);
setMessageIdPool({});
}, []);
const addEventList = useCallback((events: IEventList, message_id: string) => {
setEventList((x) => {
const list = [...x, ...events];
setMessageIdPool((prev) => ({ ...prev, [message_id]: list }));
return list;
});
}, []);
const currentEventListWithoutMessage = useMemo(() => {
const list = messageIdPool[currentMessageId]?.filter(
(x) =>
x.message_id === currentMessageId &&
ExcludeTypes.every((y) => y !== x.event),
);
return list as INodeEvent[];
}, [currentMessageId, messageIdPool]);
const currentEventListWithoutMessageById = useCallback(
(messageId: string) => {
const list = messageIdPool[messageId]?.filter(
(x) =>
x.message_id === messageId &&
ExcludeTypes.every((y) => y !== x.event),
);
return list as INodeEvent[];
},
[messageIdPool],
);
return {
eventList,
currentEventListWithoutMessage,
currentEventListWithoutMessageById,
setEventList,
clearEventList,
addEventList,
filterEventListByEventType,
filterEventListByMessageId,
setCurrentMessageId,
currentMessageId,
};
}

View File

@ -0,0 +1,120 @@
import message from '@/components/ui/message';
import { trim } from 'lodash';
import {
ChangeEvent,
Dispatch,
SetStateAction,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import { Operator } from '../constant';
import useGraphStore from '../store';
import { getAgentNodeTools } from '../utils';
export function useHandleTooNodeNameChange({
id,
name,
setName,
}: {
id?: string;
name?: string;
setName: Dispatch<SetStateAction<string>>;
}) {
const { clickedToolId, findUpstreamNodeById, updateNodeForm } = useGraphStore(
(state) => state,
);
const agentNode = findUpstreamNodeById(id);
const tools = getAgentNodeTools(agentNode);
const previousName = useMemo(() => {
const tool = tools.find((x) => x.component_name === clickedToolId);
return tool?.name || tool?.component_name;
}, [clickedToolId, tools]);
const handleToolNameBlur = useCallback(() => {
const trimmedName = trim(name);
const existsSameName = tools.some((x) => x.name === trimmedName);
if (trimmedName === '' || existsSameName) {
if (existsSameName && previousName !== name) {
message.error('The name cannot be repeated');
}
setName(previousName || '');
return;
}
if (agentNode?.id) {
const nextTools = tools.map((x) => {
if (x.component_name === clickedToolId) {
return {
...x,
name,
};
}
return x;
});
updateNodeForm(agentNode?.id, nextTools, ['tools']);
}
}, [
agentNode?.id,
clickedToolId,
name,
previousName,
setName,
tools,
updateNodeForm,
]);
return { handleToolNameBlur, previousToolName: previousName };
}
export const useHandleNodeNameChange = ({
id,
data,
}: {
id?: string;
data: any;
}) => {
const [name, setName] = useState<string>('');
const { updateNodeName, nodes, getOperatorTypeFromId } = useGraphStore(
(state) => state,
);
const previousName = data?.name;
const isToolNode = getOperatorTypeFromId(id) === Operator.Tool;
const { handleToolNameBlur, previousToolName } = useHandleTooNodeNameChange({
id,
name,
setName,
});
const handleNameBlur = useCallback(() => {
const existsSameName = nodes.some((x) => x.data.name === name);
if (trim(name) === '' || existsSameName) {
if (existsSameName && previousName !== name) {
message.error('The name cannot be repeated');
}
setName(previousName);
return;
}
if (id) {
updateNodeName(id, name);
}
}, [name, id, updateNodeName, previousName, nodes]);
const handleNameChange = useCallback((e: ChangeEvent<any>) => {
setName(e.target.value);
}, []);
useEffect(() => {
setName(isToolNode ? previousToolName : previousName);
}, [isToolNode, previousName, previousToolName]);
return {
name,
handleNameBlur: isToolNode ? handleToolNameBlur : handleNameBlur,
handleNameChange,
};
};

View File

@ -0,0 +1,60 @@
import { MessageType } from '@/constants/chat';
import { Message } from '@/interfaces/database/chat';
import { IMessage } from '@/pages/chat/interface';
import { get } from 'lodash';
import { useCallback, useMemo } from 'react';
import { BeginQuery } from '../interface';
import { buildBeginQueryWithObject } from '../utils';
type IAwaitCompentData = {
derivedMessages: IMessage[];
sendFormMessage: (params: {
inputs: Record<string, BeginQuery>;
id: string;
}) => void;
canvasId: string;
};
const useAwaitCompentData = (props: IAwaitCompentData) => {
const { derivedMessages, sendFormMessage, canvasId } = props;
const getInputs = useCallback((message: Message) => {
return get(message, 'data.inputs', {}) as Record<string, BeginQuery>;
}, []);
const buildInputList = useCallback(
(message: Message) => {
return Object.entries(getInputs(message)).map(([key, val]) => {
return {
...val,
key,
};
});
},
[getInputs],
);
const handleOk = useCallback(
(message: Message) => (values: BeginQuery[]) => {
const inputs = getInputs(message);
const nextInputs = buildBeginQueryWithObject(inputs, values);
sendFormMessage({
inputs: nextInputs,
id: canvasId,
});
},
[getInputs, sendFormMessage, canvasId],
);
const isWaitting = useMemo(() => {
const temp = derivedMessages?.some((message, i) => {
const flag =
message.role === MessageType.Assistant &&
derivedMessages.length - 1 === i &&
message.data;
return flag;
});
return temp;
}, [derivedMessages]);
return { getInputs, buildInputList, handleOk, isWaitting };
};
export { useAwaitCompentData };

View File

@ -0,0 +1,71 @@
import { useToast } from '@/components/hooks/use-toast';
import { FileMimeType, Platform } from '@/constants/common';
import { useSetModalState } from '@/hooks/common-hooks';
import { useFetchAgent } from '@/hooks/use-agent-request';
import { IGraph } from '@/interfaces/database/flow';
import { downloadJsonFile } from '@/utils/file-util';
import { message } from 'antd';
import isEmpty from 'lodash/isEmpty';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useBuildDslData } from './use-build-dsl';
import { useSetGraphInfo } from './use-set-graph';
export const useHandleExportOrImportJsonFile = () => {
const { buildDslData } = useBuildDslData();
const {
visible: fileUploadVisible,
hideModal: hideFileUploadModal,
showModal: showFileUploadModal,
} = useSetModalState();
const setGraphInfo = useSetGraphInfo();
const { data } = useFetchAgent();
const { t } = useTranslation();
const { toast } = useToast();
const onFileUploadOk = useCallback(
async ({
fileList,
platform,
}: {
fileList: File[];
platform: Platform;
}) => {
console.log('🚀 ~ useHandleExportOrImportJsonFile ~ platform:', platform);
if (fileList.length > 0) {
const file = fileList[0];
if (file.type !== FileMimeType.Json) {
toast({ title: t('flow.jsonUploadTypeErrorMessage') });
return;
}
const graphStr = await file.text();
const errorMessage = t('flow.jsonUploadContentErrorMessage');
try {
const graph = JSON.parse(graphStr);
if (graphStr && !isEmpty(graph) && Array.isArray(graph?.nodes)) {
setGraphInfo(graph ?? ({} as IGraph));
hideFileUploadModal();
} else {
message.error(errorMessage);
}
} catch (error) {
message.error(errorMessage);
}
}
},
[hideFileUploadModal, setGraphInfo, t, toast],
);
const handleExportJson = useCallback(() => {
downloadJsonFile(buildDslData().graph, `${data.title}.json`);
}, [buildDslData, data.title]);
return {
fileUploadVisible,
handleExportJson,
handleImportJson: showFileUploadModal,
hideFileUploadModal,
onFileUploadOk,
};
};

View File

@ -0,0 +1,19 @@
import { useFetchAgent } from '@/hooks/use-agent-request';
import { IGraph } from '@/interfaces/database/flow';
import { useEffect } from 'react';
import { useSetGraphInfo } from './use-set-graph';
export const useFetchDataOnMount = () => {
const { loading, data, refetch } = useFetchAgent();
const setGraphInfo = useSetGraphInfo();
useEffect(() => {
setGraphInfo(data?.dsl?.graph ?? ({} as IGraph));
}, [setGraphInfo, data]);
useEffect(() => {
refetch();
}, [refetch]);
return { loading, flowDetail: data };
};

View File

@ -0,0 +1,12 @@
import { useListMcpServer } from '@/hooks/use-mcp-request';
export function useFindMcpById() {
const { data } = useListMcpServer();
const findMcpById = (id: string) =>
data.mcp_servers.find((item) => item.id === id);
return {
findMcpById,
};
}

View File

@ -0,0 +1,20 @@
import { RAGFlowNodeType } from '@/interfaces/database/flow';
import { isEmpty } from 'lodash';
import { useMemo } from 'react';
export function useFormValues(
defaultValues: Record<string, any>,
node?: RAGFlowNodeType,
) {
const values = useMemo(() => {
const formData = node?.data?.form;
if (isEmpty(formData)) {
return defaultValues;
}
return formData;
}, [defaultValues, node?.data?.form]);
return values;
}

View File

@ -0,0 +1,317 @@
import { AgentGlobals } from '@/constants/agent';
import { useFetchAgent } from '@/hooks/use-agent-request';
import { RAGFlowNodeType } from '@/interfaces/database/flow';
import { Edge } from '@xyflow/react';
import { DefaultOptionType } from 'antd/es/select';
import { t } from 'i18next';
import { isEmpty } from 'lodash';
import get from 'lodash/get';
import {
ReactNode,
useCallback,
useContext,
useEffect,
useMemo,
useState,
} from 'react';
import {
AgentDialogueMode,
BeginId,
BeginQueryType,
Operator,
VariableType,
} from '../constant';
import { AgentFormContext } from '../context';
import { buildBeginInputListFromObject } from '../form/begin-form/utils';
import { BeginQuery } from '../interface';
import OperatorIcon from '../operator-icon';
import useGraphStore from '../store';
export function useSelectBeginNodeDataInputs() {
const getNode = useGraphStore((state) => state.getNode);
return buildBeginInputListFromObject(
getNode(BeginId)?.data?.form?.inputs ?? {},
);
}
export function useIsTaskMode() {
const getNode = useGraphStore((state) => state.getNode);
return useMemo(() => {
const node = getNode(BeginId);
return node?.data?.form?.mode === AgentDialogueMode.Task;
}, [getNode]);
}
export const useGetBeginNodeDataQuery = () => {
const getNode = useGraphStore((state) => state.getNode);
const getBeginNodeDataQuery = useCallback(() => {
return buildBeginInputListFromObject(
get(getNode(BeginId), 'data.form.inputs', {}),
);
}, [getNode]);
return getBeginNodeDataQuery;
};
export const useGetBeginNodeDataInputs = () => {
const getNode = useGraphStore((state) => state.getNode);
const inputs = get(getNode(BeginId), 'data.form.inputs', {});
const beginNodeDataInputs = useMemo(() => {
return buildBeginInputListFromObject(inputs);
}, [inputs]);
return beginNodeDataInputs;
};
export const useGetBeginNodeDataQueryIsSafe = () => {
const [isBeginNodeDataQuerySafe, setIsBeginNodeDataQuerySafe] =
useState(false);
const inputs = useSelectBeginNodeDataInputs();
const nodes = useGraphStore((state) => state.nodes);
useEffect(() => {
const query: BeginQuery[] = inputs;
const isSafe = !query.some((q) => !q.optional && q.type === 'file');
setIsBeginNodeDataQuerySafe(isSafe);
}, [inputs, nodes]);
return isBeginNodeDataQuerySafe;
};
function filterAllUpstreamNodeIds(edges: Edge[], nodeIds: string[]) {
return nodeIds.reduce<string[]>((pre, nodeId) => {
const currentEdges = edges.filter((x) => x.target === nodeId);
const upstreamNodeIds: string[] = currentEdges.map((x) => x.source);
const ids = upstreamNodeIds.concat(
filterAllUpstreamNodeIds(edges, upstreamNodeIds),
);
ids.forEach((x) => {
if (pre.every((y) => y !== x)) {
pre.push(x);
}
});
return pre;
}, []);
}
export function buildOutputOptions(
outputs: Record<string, any> = {},
nodeId?: string,
parentLabel?: string | ReactNode,
icon?: ReactNode,
) {
return Object.keys(outputs).map((x) => ({
label: x,
value: `${nodeId}@${x}`,
parentLabel,
icon,
type: outputs[x]?.type,
}));
}
export function useBuildNodeOutputOptions(nodeId?: string) {
const nodes = useGraphStore((state) => state.nodes);
const edges = useGraphStore((state) => state.edges);
const nodeOutputOptions = useMemo(() => {
if (!nodeId) {
return [];
}
const upstreamIds = filterAllUpstreamNodeIds(edges, [nodeId]);
const nodeWithOutputList = nodes.filter(
(x) =>
upstreamIds.some((y) => y === x.id) && !isEmpty(x.data?.form?.outputs),
);
return nodeWithOutputList
.filter((x) => x.id !== nodeId)
.map((x) => ({
label: x.data.name,
value: x.id,
title: x.data.name,
options: buildOutputOptions(
x.data.form.outputs,
x.id,
x.data.name,
<OperatorIcon name={x.data.label as Operator} />,
),
}));
}, [edges, nodeId, nodes]);
return nodeOutputOptions;
}
// exclude nodes with branches
const ExcludedNodes = [
Operator.Categorize,
Operator.Relevant,
Operator.Begin,
Operator.Note,
];
const StringList = [
BeginQueryType.Line,
BeginQueryType.Paragraph,
BeginQueryType.Options,
];
function transferToVariableType(type: string) {
if (StringList.some((x) => x === type)) {
return VariableType.String;
}
return type;
}
export function useBuildBeginVariableOptions() {
const inputs = useSelectBeginNodeDataInputs();
const options = useMemo(() => {
return [
{
label: <span>{t('flow.beginInput')}</span>,
title: t('flow.beginInput'),
options: inputs.map((x) => ({
label: x.name,
parentLabel: <span>{t('flow.beginInput')}</span>,
icon: <OperatorIcon name={Operator.Begin} className="block" />,
value: `begin@${x.key}`,
type: transferToVariableType(x.type),
})),
},
];
}, [inputs]);
return options;
}
export const useBuildVariableOptions = (nodeId?: string, parentId?: string) => {
const nodeOutputOptions = useBuildNodeOutputOptions(nodeId);
const parentNodeOutputOptions = useBuildNodeOutputOptions(parentId);
const beginOptions = useBuildBeginVariableOptions();
const options = useMemo(() => {
return [...beginOptions, ...nodeOutputOptions, ...parentNodeOutputOptions];
}, [beginOptions, nodeOutputOptions, parentNodeOutputOptions]);
return options;
};
export function useBuildQueryVariableOptions(n?: RAGFlowNodeType) {
const { data } = useFetchAgent();
const node = useContext(AgentFormContext) || n;
const options = useBuildVariableOptions(node?.id, node?.parentId);
const nextOptions = useMemo(() => {
const globals = data?.dsl?.globals ?? {};
const globalOptions = Object.entries(globals).map(([key, value]) => ({
label: key,
value: key,
icon: <OperatorIcon name={Operator.Begin} className="block" />,
parentLabel: <span>{t('flow.beginInput')}</span>,
type: Array.isArray(value)
? `${VariableType.Array}${key === AgentGlobals.SysFiles ? '<file>' : ''}`
: typeof value,
}));
return [
{ ...options[0], options: [...options[0]?.options, ...globalOptions] },
...options.slice(1),
];
}, [data.dsl?.globals, options]);
return nextOptions;
}
export function useBuildComponentIdOptions(nodeId?: string, parentId?: string) {
const nodes = useGraphStore((state) => state.nodes);
// Limit the nodes inside iteration to only reference peer nodes with the same parentId and other external nodes other than their parent nodes
const filterChildNodesToSameParentOrExternal = useCallback(
(node: RAGFlowNodeType) => {
// Node inside iteration
if (parentId) {
return (
(node.parentId === parentId || node.parentId === undefined) &&
node.id !== parentId
);
}
return node.parentId === undefined; // The outermost node
},
[parentId],
);
const componentIdOptions = useMemo(() => {
return nodes
.filter(
(x) =>
x.id !== nodeId &&
!ExcludedNodes.some((y) => y === x.data.label) &&
filterChildNodesToSameParentOrExternal(x),
)
.map((x) => ({ label: x.data.name, value: x.id }));
}, [nodes, nodeId, filterChildNodesToSameParentOrExternal]);
return [
{
label: <span>Component Output</span>,
title: 'Component Output',
options: componentIdOptions,
},
];
}
export function useBuildComponentIdAndBeginOptions(
nodeId?: string,
parentId?: string,
) {
const componentIdOptions = useBuildComponentIdOptions(nodeId, parentId);
const beginOptions = useBuildBeginVariableOptions();
return [...beginOptions, ...componentIdOptions];
}
export const useGetComponentLabelByValue = (nodeId: string) => {
const options = useBuildComponentIdAndBeginOptions(nodeId);
const flattenOptions = useMemo(() => {
return options.reduce<DefaultOptionType[]>((pre, cur) => {
return [...pre, ...cur.options];
}, []);
}, [options]);
const getLabel = useCallback(
(val?: string) => {
return flattenOptions.find((x) => x.value === val)?.label;
},
[flattenOptions],
);
return getLabel;
};
export function useGetVariableLabelByValue(nodeId: string) {
const { getNode } = useGraphStore((state) => state);
const nextOptions = useBuildQueryVariableOptions(getNode(nodeId));
const flattenOptions = useMemo(() => {
return nextOptions.reduce<DefaultOptionType[]>((pre, cur) => {
return [...pre, ...cur.options];
}, []);
}, [nextOptions]);
const getLabel = useCallback(
(val?: string) => {
return flattenOptions.find((x) => x.value === val)?.label;
},
[flattenOptions],
);
return getLabel;
}

View File

@ -0,0 +1,35 @@
import { useMouse } from 'ahooks';
import { useCallback, useEffect, useRef, useState } from 'react';
export function useMoveNote() {
const ref = useRef<SVGSVGElement>(null);
const mouse = useMouse();
const [imgVisible, setImgVisible] = useState(false);
const toggleVisible = useCallback((visible: boolean) => {
setImgVisible(visible);
}, []);
const showImage = useCallback(() => {
toggleVisible(true);
}, [toggleVisible]);
const hideImage = useCallback(() => {
toggleVisible(false);
}, [toggleVisible]);
useEffect(() => {
if (ref.current) {
ref.current.style.top = `${mouse.clientY - 70}px`;
ref.current.style.left = `${mouse.clientX + 10}px`;
}
}, [mouse.clientX, mouse.clientY]);
return {
ref,
showImage,
hideImage,
mouse,
imgVisible,
};
}

View File

@ -0,0 +1,12 @@
import { useCallback } from 'react';
export function useOpenDocument() {
const openDocument = useCallback(() => {
window.open(
'https://ragflow.io/docs/dev/category/agent-components',
'_blank',
);
}, []);
return openDocument;
}

View File

@ -0,0 +1,89 @@
import {
useFetchAgent,
useResetAgent,
useSetAgent,
} from '@/hooks/use-agent-request';
import { RAGFlowNodeType } from '@/interfaces/database/flow';
import { formatDate } from '@/utils/date';
import { useDebounceEffect } from 'ahooks';
import { useCallback, useEffect, useState } from 'react';
import { useParams } from 'umi';
import useGraphStore from '../store';
import { useBuildDslData } from './use-build-dsl';
export const useSaveGraph = (showMessage: boolean = true) => {
const { data } = useFetchAgent();
const { setAgent, loading } = useSetAgent(showMessage);
const { id } = useParams();
const { buildDslData } = useBuildDslData();
const saveGraph = useCallback(
async (currentNodes?: RAGFlowNodeType[]) => {
return setAgent({
id,
title: data.title,
dsl: buildDslData(currentNodes),
});
},
[setAgent, data, id, buildDslData],
);
return { saveGraph, loading };
};
export const useSaveGraphBeforeOpeningDebugDrawer = (show: () => void) => {
const { saveGraph, loading } = useSaveGraph();
const { resetAgent } = useResetAgent();
const handleRun = useCallback(
async (nextNodes?: RAGFlowNodeType[]) => {
const saveRet = await saveGraph(nextNodes);
if (saveRet?.code === 0) {
// Call the reset api before opening the run drawer each time
const resetRet = await resetAgent();
// After resetting, all previous messages will be cleared.
if (resetRet?.code === 0) {
show();
}
}
},
[saveGraph, resetAgent, show],
);
return { handleRun, loading };
};
export const useWatchAgentChange = (chatDrawerVisible: boolean) => {
const [time, setTime] = useState<string>();
const nodes = useGraphStore((state) => state.nodes);
const edges = useGraphStore((state) => state.edges);
const { saveGraph } = useSaveGraph(false);
const { data: flowDetail } = useFetchAgent();
const setSaveTime = useCallback((updateTime: number) => {
setTime(formatDate(updateTime));
}, []);
useEffect(() => {
setSaveTime(flowDetail?.update_time);
}, [flowDetail, setSaveTime]);
const saveAgent = useCallback(async () => {
if (!chatDrawerVisible) {
const ret = await saveGraph();
setSaveTime(ret.data.update_time);
}
}, [chatDrawerVisible, saveGraph, setSaveTime]);
useDebounceEffect(
() => {
saveAgent();
},
[nodes, edges],
{
wait: 1000 * 20,
},
);
return time;
};

View File

@ -0,0 +1,79 @@
import { SharedFrom } from '@/constants/chat';
import { useSetModalState } from '@/hooks/common-hooks';
import { IEventList } from '@/hooks/use-send-message';
import {
buildRequestBody,
useSendAgentMessage,
} from '@/pages/agent/chat/use-send-agent-message';
import trim from 'lodash/trim';
import { useCallback, useState } from 'react';
import { useSearchParams } from 'umi';
export const useSendButtonDisabled = (value: string) => {
return trim(value) === '';
};
export const useGetSharedChatSearchParams = () => {
const [searchParams] = useSearchParams();
const data_prefix = 'data_';
const data = Object.fromEntries(
searchParams
.entries()
.filter(([key]) => key.startsWith(data_prefix))
.map(([key, value]) => [key.replace(data_prefix, ''), value]),
);
return {
from: searchParams.get('from') as SharedFrom,
sharedId: searchParams.get('shared_id'),
locale: searchParams.get('locale'),
data: data,
visibleAvatar: searchParams.get('visible_avatar')
? searchParams.get('visible_avatar') !== '1'
: true,
};
};
export const useSendNextSharedMessage = (
addEventList: (data: IEventList, messageId: string) => void,
isTaskMode: boolean,
) => {
const { from, sharedId: conversationId } = useGetSharedChatSearchParams();
const url = `/api/v1/${from === SharedFrom.Agent ? 'agentbots' : 'chatbots'}/${conversationId}/completions`;
const [params, setParams] = useState<any[]>([]);
const {
visible: parameterDialogVisible,
hideModal: hideParameterDialog,
showModal: showParameterDialog,
} = useSetModalState();
const ret = useSendAgentMessage(url, addEventList, params, true);
const ok = useCallback(
(params: any[]) => {
if (isTaskMode) {
const msgBody = buildRequestBody('');
ret.sendMessage({
message: msgBody,
beginInputs: params,
});
} else {
setParams(params);
}
hideParameterDialog();
},
[hideParameterDialog, isTaskMode, ret],
);
return {
...ret,
hasError: false,
parameterDialogVisible,
hideParameterDialog,
showParameterDialog,
ok,
};
};

View File

@ -0,0 +1,17 @@
import { IGraph } from '@/interfaces/database/flow';
import { useCallback } from 'react';
import useGraphStore from '../store';
export const useSetGraphInfo = () => {
const { setEdges, setNodes } = useGraphStore((state) => state);
const setGraphInfo = useCallback(
({ nodes = [], edges = [] }: IGraph) => {
if (nodes.length || edges.length) {
setNodes(nodes);
setEdges(edges);
}
},
[setEdges, setNodes],
);
return setGraphInfo;
};

View File

@ -0,0 +1,91 @@
import { useFetchTokenListBeforeOtherStep } from '@/components/embed-dialog/use-show-embed-dialog';
import { SharedFrom } from '@/constants/chat';
import { useShowDeleteConfirm } from '@/hooks/common-hooks';
import {
useCreateSystemToken,
useFetchSystemTokenList,
useRemoveSystemToken,
} from '@/hooks/user-setting-hooks';
import { IStats } from '@/interfaces/database/chat';
import { useQueryClient } from '@tanstack/react-query';
import { useCallback } from 'react';
export const useOperateApiKey = (idKey: string, dialogId?: string) => {
const { removeToken } = useRemoveSystemToken();
const { createToken, loading: creatingLoading } = useCreateSystemToken();
const { data: tokenList, loading: listLoading } = useFetchSystemTokenList();
const showDeleteConfirm = useShowDeleteConfirm();
const onRemoveToken = (token: string) => {
showDeleteConfirm({
onOk: () => removeToken(token),
});
};
const onCreateToken = useCallback(() => {
createToken({ [idKey]: dialogId });
}, [createToken, idKey, dialogId]);
return {
removeToken: onRemoveToken,
createToken: onCreateToken,
tokenList,
creatingLoading,
listLoading,
};
};
type ChartStatsType = {
[k in keyof IStats]: Array<{ xAxis: string; yAxis: number }>;
};
export const useSelectChartStatsList = (): ChartStatsType => {
const queryClient = useQueryClient();
const data = queryClient.getQueriesData({ queryKey: ['fetchStats'] });
const stats: IStats = (data.length > 0 ? data[0][1] : {}) as IStats;
return Object.keys(stats).reduce((pre, cur) => {
const item = stats[cur as keyof IStats];
if (item.length > 0) {
pre[cur as keyof IStats] = item.map((x) => ({
xAxis: x[0] as string,
yAxis: x[1] as number,
}));
}
return pre;
}, {} as ChartStatsType);
};
const getUrlWithToken = (token: string, from: string = 'chat') => {
const { protocol, host } = window.location;
return `${protocol}//${host}/chat/share?shared_id=${token}&from=${from}`;
};
export const usePreviewChat = (idKey: string) => {
const { handleOperate } = useFetchTokenListBeforeOtherStep();
const open = useCallback(
(t: string) => {
window.open(
getUrlWithToken(
t,
idKey === 'canvasId' ? SharedFrom.Agent : SharedFrom.Chat,
),
'_blank',
);
},
[idKey],
);
const handlePreview = useCallback(async () => {
const token = await handleOperate();
if (token) {
open(token);
}
}, [handleOperate, open]);
return {
handlePreview,
};
};

View File

@ -0,0 +1,186 @@
import { useSetModalState } from '@/hooks/common-hooks';
import { NodeMouseHandler } from '@xyflow/react';
import get from 'lodash/get';
import React, { useCallback, useEffect } from 'react';
import { Operator } from '../constant';
import useGraphStore from '../store';
import { useCacheChatLog } from './use-cache-chat-log';
import { useGetBeginNodeDataInputs } from './use-get-begin-query';
import { useSaveGraph } from './use-save-graph';
export const useShowFormDrawer = () => {
const {
clickedNodeId: clickNodeId,
setClickedNodeId,
getNode,
setClickedToolId,
} = useGraphStore((state) => state);
const {
visible: formDrawerVisible,
hideModal: hideFormDrawer,
showModal: showFormDrawer,
} = useSetModalState();
const handleShow = useCallback(
(e: React.MouseEvent<Element>, nodeId: string) => {
const tool = get(e.target, 'dataset.tool');
// TODO: Operator type judgment should be used
if (nodeId.startsWith(Operator.Tool) && !tool) {
return;
}
setClickedNodeId(nodeId);
setClickedToolId(tool);
showFormDrawer();
},
[setClickedNodeId, setClickedToolId, showFormDrawer],
);
return {
formDrawerVisible,
hideFormDrawer,
showFormDrawer: handleShow,
clickedNode: getNode(clickNodeId),
};
};
export const useShowSingleDebugDrawer = () => {
const { visible, showModal, hideModal } = useSetModalState();
const { saveGraph } = useSaveGraph();
const showSingleDebugDrawer = useCallback(async () => {
const saveRet = await saveGraph();
if (saveRet?.code === 0) {
showModal();
}
}, [saveGraph, showModal]);
return {
singleDebugDrawerVisible: visible,
hideSingleDebugDrawer: hideModal,
showSingleDebugDrawer,
};
};
const ExcludedNodes = [Operator.Note];
export function useShowDrawer({
drawerVisible,
hideDrawer,
}: {
drawerVisible: boolean;
hideDrawer(): void;
}) {
const {
visible: runVisible,
showModal: showRunModal,
hideModal: hideRunModal,
} = useSetModalState();
const {
visible: chatVisible,
showModal: showChatModal,
hideModal: hideChatModal,
} = useSetModalState();
const {
singleDebugDrawerVisible,
showSingleDebugDrawer,
hideSingleDebugDrawer,
} = useShowSingleDebugDrawer();
const { formDrawerVisible, hideFormDrawer, showFormDrawer, clickedNode } =
useShowFormDrawer();
const inputs = useGetBeginNodeDataInputs();
useEffect(() => {
if (drawerVisible) {
if (inputs.length > 0) {
showRunModal();
hideChatModal();
} else {
showChatModal();
hideRunModal();
}
}
}, [
hideChatModal,
hideRunModal,
showChatModal,
showRunModal,
drawerVisible,
inputs,
]);
const hideRunOrChatDrawer = useCallback(() => {
hideChatModal();
hideRunModal();
hideDrawer();
}, [hideChatModal, hideDrawer, hideRunModal]);
const onPaneClick = useCallback(() => {
hideFormDrawer();
}, [hideFormDrawer]);
const onNodeClick: NodeMouseHandler = useCallback(
(e, node) => {
if (!ExcludedNodes.some((x) => x === node.data.label)) {
hideSingleDebugDrawer();
// hideRunOrChatDrawer();
showFormDrawer(e, node.id);
}
// handle single debug icon click
if (
get(e.target, 'dataset.play') === 'true' ||
get(e.target, 'parentNode.dataset.play') === 'true'
) {
showSingleDebugDrawer();
}
},
[hideSingleDebugDrawer, showFormDrawer, showSingleDebugDrawer],
);
return {
chatVisible,
runVisible,
onPaneClick,
singleDebugDrawerVisible,
showSingleDebugDrawer,
hideSingleDebugDrawer,
formDrawerVisible,
showFormDrawer,
clickedNode,
onNodeClick,
hideFormDrawer,
hideRunOrChatDrawer,
showChatModal,
};
}
export function useShowLogSheet({
setCurrentMessageId,
}: Pick<ReturnType<typeof useCacheChatLog>, 'setCurrentMessageId'>) {
const { visible, showModal, hideModal } = useSetModalState();
const handleShow = useCallback(
(messageId: string) => {
setCurrentMessageId(messageId);
showModal();
},
[setCurrentMessageId, showModal],
);
return {
logSheetVisible: visible,
hideLogSheet: hideModal,
showLogSheet: handleShow,
};
}
export function useHideFormSheetOnNodeDeletion({
hideFormDrawer,
}: Pick<ReturnType<typeof useShowFormDrawer>, 'hideFormDrawer'>) {
const { nodes, clickedNodeId } = useGraphStore((state) => state);
useEffect(() => {
if (!nodes.some((x) => x.id === clickedNodeId)) {
hideFormDrawer();
}
}, [clickedNodeId, hideFormDrawer, nodes]);
}

View File

@ -0,0 +1,18 @@
import { useEffect } from 'react';
import { UseFormReturn, useWatch } from 'react-hook-form';
import useGraphStore from '../store';
export function useWatchFormChange(id?: string, form?: UseFormReturn<any>) {
let values = useWatch({ control: form?.control });
const updateNodeForm = useGraphStore((state) => state.updateNodeForm);
useEffect(() => {
// Manually triggered form updates are synchronized to the canvas
if (id) {
values = form?.getValues() || {};
let nextValues: any = values;
updateNodeForm(id, nextValues);
}
}, [form?.formState.isDirty, id, updateNodeForm, values]);
}