diff --git a/web/src/components/json-edit/index.tsx b/web/src/components/json-edit/index.tsx index 2ab49c4e..31586c59 100644 --- a/web/src/components/json-edit/index.tsx +++ b/web/src/components/json-edit/index.tsx @@ -1,8 +1,9 @@ -import React, { useEffect, useRef } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import './css/cloud9_night.less'; import './css/index.less'; import { JsonEditorOptions, JsonEditorProps } from './interface'; + const defaultConfig: JsonEditorOptions = { mode: 'code', modes: ['tree', 'code'], @@ -14,6 +15,7 @@ const defaultConfig: JsonEditorOptions = { enableTransform: false, indentation: 2, }; + const JsonEditor: React.FC = ({ value, onChange, @@ -25,43 +27,62 @@ const JsonEditor: React.FC = ({ const editorRef = useRef(null); const { i18n } = useTranslation(); const currentLanguageRef = useRef(i18n.language); + const [isLoading, setIsLoading] = useState(true); useEffect(() => { - if (typeof window !== 'undefined') { - const JSONEditor = require('jsoneditor'); - import('jsoneditor/dist/jsoneditor.min.css'); + let isMounted = true; - if (containerRef.current) { - // Default configuration options - const defaultOptions: JsonEditorOptions = { - ...defaultConfig, - language: i18n.language === 'zh' ? 'zh-CN' : 'en', - onChange: () => { - if (editorRef.current && onChange) { - try { - const updatedJson = editorRef.current.get(); - onChange(updatedJson); - } catch (err) { - // Do not trigger onChange when parsing error occurs - console.error(err); - } + const initEditor = async () => { + if (typeof window !== 'undefined') { + try { + const JSONEditorModule = await import('jsoneditor'); + const JSONEditor = JSONEditorModule.default || JSONEditorModule; + + await import('jsoneditor/dist/jsoneditor.min.css'); + + if (isMounted && containerRef.current) { + // Default configuration options + const defaultOptions: JsonEditorOptions = { + ...defaultConfig, + language: i18n.language === 'zh' ? 'zh-CN' : 'en', + onChange: () => { + if (editorRef.current && onChange) { + try { + const updatedJson = editorRef.current.get(); + onChange(updatedJson); + } catch (err) { + // Do not trigger onChange when parsing error occurs + console.error(err); + } + } + }, + ...options, // Merge user provided options with defaults + }; + + editorRef.current = new JSONEditor( + containerRef.current, + defaultOptions, + ); + + if (value) { + editorRef.current.set(value); } - }, - ...options, // Merge user provided options with defaults - }; - editorRef.current = new JSONEditor( - containerRef.current, - defaultOptions, - ); - - if (value) { - editorRef.current.set(value); + setIsLoading(false); + } + } catch (error) { + console.error('Failed to load jsoneditor:', error); + if (isMounted) { + setIsLoading(false); + } } } - } + }; + + initEditor(); return () => { + isMounted = false; if (editorRef.current) { if (typeof editorRef.current.destroy === 'function') { editorRef.current.destroy(); @@ -92,26 +113,38 @@ const JsonEditor: React.FC = ({ } // Recreate the editor with new language - const JSONEditor = require('jsoneditor'); + const initEditorWithNewLanguage = async () => { + try { + const JSONEditorModule = await import('jsoneditor'); + const JSONEditor = JSONEditorModule.default || JSONEditorModule; - const newOptions: JsonEditorOptions = { - ...defaultConfig, - language: i18n.language === 'zh' ? 'zh-CN' : 'en', - onChange: () => { - if (editorRef.current && onChange) { - try { - const updatedJson = editorRef.current.get(); - onChange(updatedJson); - } catch (err) { - // Do not trigger onChange when parsing error occurs - } - } - }, - ...options, // Merge user provided options with defaults + const newOptions: JsonEditorOptions = { + ...defaultConfig, + language: i18n.language === 'zh' ? 'zh-CN' : 'en', + onChange: () => { + if (editorRef.current && onChange) { + try { + const updatedJson = editorRef.current.get(); + onChange(updatedJson); + } catch (err) { + // Do not trigger onChange when parsing error occurs + } + } + }, + ...options, // Merge user provided options with defaults + }; + + editorRef.current = new JSONEditor(containerRef.current, newOptions); + editorRef.current.set(currentData); + } catch (error) { + console.error( + 'Failed to reload jsoneditor with new language:', + error, + ); + } }; - editorRef.current = new JSONEditor(containerRef.current, newOptions); - editorRef.current.set(currentData); + initEditorWithNewLanguage(); } }, [i18n.language, value, onChange, options]); @@ -135,7 +168,13 @@ const JsonEditor: React.FC = ({ ref={containerRef} style={{ height }} className={`ace-tomorrow-night w-full border border-border-button rounded-lg overflow-hidden bg-bg-input ${className} `} - /> + > + {isLoading && ( +
+
Loading editor...
+
+ )} + ); }; diff --git a/web/src/custom.d.ts b/web/src/custom.d.ts index f73d61b3..dafdf09f 100644 --- a/web/src/custom.d.ts +++ b/web/src/custom.d.ts @@ -2,3 +2,9 @@ declare module '*.md' { const content: string; export default content; } + +declare module 'jsoneditor' { + const JSONEditor: any; + export default JSONEditor; + export = JSONEditor; +} diff --git a/web/src/pages/agent/canvas/index.tsx b/web/src/pages/agent/canvas/index.tsx index 7be70be8..6b954d06 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, @@ -166,6 +167,8 @@ function AgentCanvas({ drawerVisible, hideDrawer }: IProps) { }); const [lastSendLoading, setLastSendLoading] = useState(false); + const [currentSendLoading, setCurrentSendLoading] = useState(false); + const { handleBeforeDelete } = useBeforeDelete(); const { addCanvasNode, addNoteNode } = useAddNode(reactFlowInstance); @@ -182,6 +185,7 @@ function AgentCanvas({ drawerVisible, hideDrawer }: IProps) { }, [chatVisible, clearEventList, currentTaskId, stopMessage]); const setLastSendLoadingFunc = (loading: boolean, messageId: string) => { + setCurrentSendLoading(!!loading); if (messageId === currentMessageId) { setLastSendLoading(loading); } else { @@ -249,7 +253,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 48f69e5f..053ef925 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 e6bd6ba3..25b43e4f 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 d868d70f..a0705b0b 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.content', []); 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 d66c79c4..96a83050 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 830ababd..24208b8e 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 6839554d..3b9a4e5c 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 5c759a5e..b9ebeac5 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 00000000..d92702f5 --- /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 76c0b086..bea2808a 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()} >