From 8fab4bba11e303eb9a6aeff796347bdad7f1bbfc Mon Sep 17 00:00:00 2001
From: chanx <1243304602@qq.com>
Date: Thu, 4 Dec 2025 10:47:21 +0800
Subject: [PATCH] Feature:Add a loading status to the agent canvas page.
---
web/src/pages/agent/canvas/index.tsx | 22 ++++-
.../pages/agent/canvas/node/agent-node.tsx | 2 +-
.../pages/agent/canvas/node/begin-node.tsx | 2 +-
.../agent/canvas/node/categorize-node.tsx | 2 +-
.../agent/canvas/node/exit-loop-node.tsx | 2 +-
web/src/pages/agent/canvas/node/file-node.tsx | 2 +-
web/src/pages/agent/canvas/node/index.tsx | 2 +-
.../pages/agent/canvas/node/message-node.tsx | 2 +-
.../pages/agent/canvas/node/node-wrapper.tsx | 15 +++-
.../pages/agent/canvas/node/parser-node.tsx | 2 +-
.../agent/canvas/node/retrieval-node.tsx | 2 +-
.../pages/agent/canvas/node/splitter-node.tsx | 2 +-
.../pages/agent/canvas/node/switch-node.tsx | 2 +-
.../agent/canvas/node/tokenizer-node.tsx | 2 +-
web/src/pages/agent/canvas/node/tool-node.tsx | 2 +-
web/src/pages/agent/chat/box.tsx | 6 +-
web/src/pages/agent/context.ts | 15 +++-
web/src/pages/agent/form-sheet/next.tsx | 2 +-
web/src/pages/agent/hooks/use-node-loading.ts | 88 +++++++++++++++++++
web/src/pages/agent/log-sheet/index.tsx | 2 +-
20 files changed, 152 insertions(+), 24 deletions(-)
create mode 100644 web/src/pages/agent/hooks/use-node-loading.ts
diff --git a/web/src/pages/agent/canvas/index.tsx b/web/src/pages/agent/canvas/index.tsx
index 6c088809d..5cbc903dc 100644
--- a/web/src/pages/agent/canvas/index.tsx
+++ b/web/src/pages/agent/canvas/index.tsx
@@ -40,6 +40,7 @@ import { useDropdownManager } from './context';
import { AgentBackground } from '@/components/canvas/background';
import Spotlight from '@/components/spotlight';
+import { useNodeLoading } from '../hooks/use-node-loading';
import {
useHideFormSheetOnNodeDeletion,
useShowDrawer,
@@ -172,6 +173,8 @@ function AgentCanvas({ drawerVisible, hideDrawer }: IProps) {
});
const [lastSendLoading, setLastSendLoading] = useState(false);
+ const [currentSendLoading, setCurrentSendLoading] = useState(false);
+
const { handleBeforeDelete } = useBeforeDelete();
const { addCanvasNode, addNoteNode } = useAddNode(reactFlowInstance);
@@ -188,6 +191,7 @@ function AgentCanvas({ drawerVisible, hideDrawer }: IProps) {
}, [chatVisible, clearEventList, currentTaskId, stopMessage]);
const setLastSendLoadingFunc = (loading: boolean, messageId: string) => {
+ setCurrentSendLoading(!!loading);
if (messageId === currentMessageId) {
setLastSendLoading(loading);
} else {
@@ -255,7 +259,10 @@ function AgentCanvas({ drawerVisible, hideDrawer }: IProps) {
clearActiveDropdown,
removePlaceholderNode,
]);
-
+ const { lastNode, setDerivedMessages, startButNotFinishedNodeIds } =
+ useNodeLoading({
+ currentEventListWithoutMessageById,
+ });
return (
-
+
)}
+
{chatVisible && (
-
+
{isHeadAgent && (
<>
diff --git a/web/src/pages/agent/canvas/node/begin-node.tsx b/web/src/pages/agent/canvas/node/begin-node.tsx
index 48f69e5f6..053ef925a 100644
--- a/web/src/pages/agent/canvas/node/begin-node.tsx
+++ b/web/src/pages/agent/canvas/node/begin-node.tsx
@@ -24,7 +24,7 @@ function InnerBeginNode({ data, id, selected }: NodeProps) {
const inputs: Record = get(data, 'form.inputs', {});
return (
-
+
-
+
diff --git a/web/src/pages/agent/canvas/node/exit-loop-node.tsx b/web/src/pages/agent/canvas/node/exit-loop-node.tsx
index e6bd6ba3d..25b43e4ff 100644
--- a/web/src/pages/agent/canvas/node/exit-loop-node.tsx
+++ b/web/src/pages/agent/canvas/node/exit-loop-node.tsx
@@ -14,7 +14,7 @@ export function ExitLoopNode({ id, data, selected }: NodeProps>) {
showRun={false}
showCopy={false}
>
-
+
diff --git a/web/src/pages/agent/canvas/node/file-node.tsx b/web/src/pages/agent/canvas/node/file-node.tsx
index d868d70fa..a0705b0b7 100644
--- a/web/src/pages/agent/canvas/node/file-node.tsx
+++ b/web/src/pages/agent/canvas/node/file-node.tsx
@@ -23,7 +23,7 @@ function InnerFileNode({ data, id, selected }: NodeProps) {
const inputs: Record = get(data, 'form.inputs', {});
return (
-
+
-
+
) {
const messages: string[] = get(data, 'form.messages', []);
return (
-
+
{/* & { selected?: boolean };
-export function NodeWrapper({ children, className, selected }: IProps) {
+export function NodeWrapper({ children, className, selected, id }: IProps) {
+ const { currentSendLoading, startButNotFinishedNodeIds = [] } =
+ useContext(AgentInstanceContext);
return (
+ {id &&
+ startButNotFinishedNodeIds.indexOf(id as string) > -1 &&
+ currentSendLoading && (
+
+
+
+ )}
{children}
);
diff --git a/web/src/pages/agent/canvas/node/parser-node.tsx b/web/src/pages/agent/canvas/node/parser-node.tsx
index d66c79c45..96a83050a 100644
--- a/web/src/pages/agent/canvas/node/parser-node.tsx
+++ b/web/src/pages/agent/canvas/node/parser-node.tsx
@@ -19,7 +19,7 @@ function ParserNode({
}: NodeProps>) {
const { t } = useTranslation();
return (
-
+
-
+
-
+
) {
const { positions } = useBuildSwitchHandlePositions({ data, id });
return (
-
+
diff --git a/web/src/pages/agent/canvas/node/tokenizer-node.tsx b/web/src/pages/agent/canvas/node/tokenizer-node.tsx
index 830ababdd..24208b8ea 100644
--- a/web/src/pages/agent/canvas/node/tokenizer-node.tsx
+++ b/web/src/pages/agent/canvas/node/tokenizer-node.tsx
@@ -27,7 +27,7 @@ function TokenizerNode({
showRun={false}
showCopy={false}
>
-
+
+
=
diff --git a/web/src/pages/agent/context.ts b/web/src/pages/agent/context.ts
index 6839554d3..3b9a4e5c9 100644
--- a/web/src/pages/agent/context.ts
+++ b/web/src/pages/agent/context.ts
@@ -1,6 +1,8 @@
+import { INodeEvent } from '@/hooks/use-send-message';
+import { IMessage } from '@/interfaces/database/chat';
import { RAGFlowNodeType } from '@/interfaces/database/flow';
import { HandleType, Position } from '@xyflow/react';
-import { createContext } from 'react';
+import { Dispatch, SetStateAction, createContext } from 'react';
import { useAddNode } from './hooks/use-add-node';
import { useCacheChatLog } from './hooks/use-cache-chat-log';
import { useShowFormDrawer, useShowLogSheet } from './hooks/use-show-drawer';
@@ -13,7 +15,11 @@ type AgentInstanceContextType = Pick<
ReturnType,
'addCanvasNode'
> &
- Pick, 'showFormDrawer'>;
+ Pick, 'showFormDrawer'> & {
+ lastNode: INodeEvent | null;
+ currentSendLoading: boolean;
+ startButNotFinishedNodeIds: string[];
+ };
export const AgentInstanceContext = createContext(
{} as AgentInstanceContextType,
@@ -22,7 +28,10 @@ export const AgentInstanceContext = createContext(
type AgentChatContextType = Pick<
ReturnType,
'showLogSheet'
-> & { setLastSendLoadingFunc: (loading: boolean, messageId: string) => void };
+> & {
+ setLastSendLoadingFunc: (loading: boolean, messageId: string) => void;
+ setDerivedMessages: Dispatch>;
+};
export const AgentChatContext = createContext(
{} as AgentChatContextType,
diff --git a/web/src/pages/agent/form-sheet/next.tsx b/web/src/pages/agent/form-sheet/next.tsx
index 5c759a5e5..b9ebeac51 100644
--- a/web/src/pages/agent/form-sheet/next.tsx
+++ b/web/src/pages/agent/form-sheet/next.tsx
@@ -55,7 +55,7 @@ const FormSheet = ({
diff --git a/web/src/pages/agent/hooks/use-node-loading.ts b/web/src/pages/agent/hooks/use-node-loading.ts
new file mode 100644
index 000000000..d92702f56
--- /dev/null
+++ b/web/src/pages/agent/hooks/use-node-loading.ts
@@ -0,0 +1,88 @@
+import {
+ INodeData,
+ INodeEvent,
+ MessageEventType,
+} from '@/hooks/use-send-message';
+import { IMessage } from '@/interfaces/database/chat';
+import { useCallback, useMemo, useState } from 'react';
+
+export const useNodeLoading = ({
+ currentEventListWithoutMessageById,
+}: {
+ currentEventListWithoutMessageById: (messageId: string) => INodeEvent[];
+}) => {
+ const [derivedMessages, setDerivedMessages] = useState();
+
+ const lastMessageId = useMemo(() => {
+ return derivedMessages?.[derivedMessages?.length - 1]?.id;
+ }, [derivedMessages]);
+
+ const currentEventListWithoutMessage = useMemo(() => {
+ if (!lastMessageId) {
+ return [];
+ }
+ return currentEventListWithoutMessageById(lastMessageId);
+ }, [currentEventListWithoutMessageById, lastMessageId]);
+
+ const startedNodeList = useMemo(() => {
+ const duplicateList = currentEventListWithoutMessage?.filter(
+ (x) => x.event === MessageEventType.NodeStarted,
+ ) as INodeEvent[];
+
+ // Remove duplicate nodes
+ return duplicateList?.reduce>((pre, cur) => {
+ if (pre.every((x) => x.data.component_id !== cur.data.component_id)) {
+ pre.push(cur);
+ }
+ return pre;
+ }, []);
+ }, [currentEventListWithoutMessage]);
+
+ const filterFinishedNodeList = useCallback(() => {
+ const nodeEventList = currentEventListWithoutMessage
+ .filter(
+ (x) => x.event === MessageEventType.NodeFinished,
+ // x.event === MessageEventType.NodeFinished &&
+ // (x.data as INodeData)?.component_id === componentId,
+ )
+ .map((x) => x.data);
+
+ return nodeEventList;
+ }, [currentEventListWithoutMessage]);
+
+ const lastNode = useMemo(() => {
+ if (!startedNodeList) {
+ return null;
+ }
+ return startedNodeList[startedNodeList.length - 1];
+ }, [startedNodeList]);
+
+ const startNodeIds = useMemo(() => {
+ if (!startedNodeList) {
+ return [];
+ }
+ return startedNodeList.map((x) => x.data.component_id);
+ }, [startedNodeList]);
+
+ const finishNodeIds = useMemo(() => {
+ if (!lastNode) {
+ return [];
+ }
+ const nodeDataList = filterFinishedNodeList();
+ const finishNodeIdsTemp = nodeDataList.map(
+ (x: INodeData) => x.component_id,
+ );
+ return Array.from(new Set(finishNodeIdsTemp));
+ }, [lastNode, filterFinishedNodeList]);
+
+ const startButNotFinishedNodeIds = useMemo(() => {
+ return startNodeIds.filter((x) => !finishNodeIds.includes(x));
+ }, [finishNodeIds, startNodeIds]);
+
+ return {
+ lastNode,
+ startButNotFinishedNodeIds,
+ filterFinishedNodeList,
+ setDerivedMessages,
+ };
+};
diff --git a/web/src/pages/agent/log-sheet/index.tsx b/web/src/pages/agent/log-sheet/index.tsx
index 76c0b0865..bea2808a2 100644
--- a/web/src/pages/agent/log-sheet/index.tsx
+++ b/web/src/pages/agent/log-sheet/index.tsx
@@ -26,7 +26,7 @@ export function LogSheet({
return (
e.preventDefault()}
>