Commit 7cca3f9f by DigHuang Committed by GitHub

refactor(chat): optimize sandbox status logic and decouple UI/Status hooks (#6713)

* refactor(chat): optimize sandbox status logic and decouple UI/Status hooks

* fix: useRef, rename onClose to afterClose
parent 8f731b55
...@@ -169,7 +169,7 @@ export type AIChatItemType = { ...@@ -169,7 +169,7 @@ export type AIChatItemType = {
citeCollectionIds?: string[]; citeCollectionIds?: string[];
/** /**
* @deprecated 不再存储在 chatItemSchema 里,分别存储到 chatItemResponseSchema * 不再存储在 chatItemSchema 里,分别存储到 chatItemResponseSchema
*/ */
[DispatchNodeResponseKeyEnum.nodeResponse]?: ChatHistoryItemResType[]; [DispatchNodeResponseKeyEnum.nodeResponse]?: ChatHistoryItemResType[];
}; };
......
...@@ -24,7 +24,7 @@ import type { HelperBotRefType } from '@/components/core/chat/HelperBot/context' ...@@ -24,7 +24,7 @@ import type { HelperBotRefType } from '@/components/core/chat/HelperBot/context'
import { HelperBotTypeEnum } from '@fastgpt/global/core/chat/helperBot/type'; import { HelperBotTypeEnum } from '@fastgpt/global/core/chat/helperBot/type';
import { loadGeneratedTools } from './utils'; import { loadGeneratedTools } from './utils';
import { systemSubInfo } from '@fastgpt/global/core/workflow/node/agent/constants'; import { systemSubInfo } from '@fastgpt/global/core/workflow/node/agent/constants';
import { useSandboxEditor } from '@/pageComponents/chat/SandboxEditor/hook'; import { useSandboxEditor, useSandboxStatus } from '@/pageComponents/chat/SandboxEditor/hook';
type Props = { type Props = {
appForm: AppFormEditFormType; appForm: AppFormEditFormType;
...@@ -50,8 +50,12 @@ const ChatTest = ({ appForm, setAppForm, setRenderEdit, form2WorkflowFn }: Props ...@@ -50,8 +50,12 @@ const ChatTest = ({ appForm, setAppForm, setRenderEdit, form2WorkflowFn }: Props
edges: appDetail.edges || [] edges: appDetail.edges || []
}); });
// Sandbox state // Sandbox: Status Hook 负责网络同步,UI Hook 负责弹窗渲染
const { SandboxEditorModal, SandboxEntryIcon } = useSandboxEditor({ const { SandboxEntryIcon } = useSandboxStatus({
appId: appDetail._id,
chatId
});
const { SandboxEditorModal, onOpenSandboxModal } = useSandboxEditor({
appId: appDetail._id, appId: appDetail._id,
chatId chatId
}); });
...@@ -126,7 +130,7 @@ const ChatTest = ({ appForm, setAppForm, setRenderEdit, form2WorkflowFn }: Props ...@@ -126,7 +130,7 @@ const ChatTest = ({ appForm, setAppForm, setRenderEdit, form2WorkflowFn }: Props
)} )}
<Box flex={1} /> <Box flex={1} />
<SandboxEntryIcon size={'smSquare'} mr={2} /> <SandboxEntryIcon size={'smSquare'} mr={2} onOpen={onOpenSandboxModal} />
<MyTooltip label={t('common:core.chat.Restart')}> <MyTooltip label={t('common:core.chat.Restart')}>
<IconButton <IconButton
className="chat" className="chat"
......
...@@ -18,7 +18,7 @@ import ChatQuoteList from '@/pageComponents/chat/ChatQuoteList'; ...@@ -18,7 +18,7 @@ import ChatQuoteList from '@/pageComponents/chat/ChatQuoteList';
import VariablePopover from '@/components/core/chat/ChatContainer/components/VariablePopover'; import VariablePopover from '@/components/core/chat/ChatContainer/components/VariablePopover';
import { ChatTypeEnum } from '@/components/core/chat/ChatContainer/ChatBox/constants'; import { ChatTypeEnum } from '@/components/core/chat/ChatContainer/ChatBox/constants';
import type { Form2WorkflowFnType } from '../FormComponent/type'; import type { Form2WorkflowFnType } from '../FormComponent/type';
import { useSandboxEditor } from '@/pageComponents/chat/SandboxEditor/hook'; import { useSandboxEditor, useSandboxStatus } from '@/pageComponents/chat/SandboxEditor/hook';
type Props = { type Props = {
appForm: AppFormEditFormType; appForm: AppFormEditFormType;
...@@ -40,8 +40,12 @@ const ChatTest = ({ appForm, setRenderEdit, form2WorkflowFn }: Props) => { ...@@ -40,8 +40,12 @@ const ChatTest = ({ appForm, setRenderEdit, form2WorkflowFn }: Props) => {
edges: appDetail.edges || [] edges: appDetail.edges || []
}); });
// Sandbox state // Sandbox: Status Hook 负责网络同步,UI Hook 负责弹窗渲染
const { SandboxEditorModal, SandboxEntryIcon, setSandboxExists } = useSandboxEditor({ const { SandboxEntryIcon, setSandboxExists } = useSandboxStatus({
appId: appDetail._id,
chatId
});
const { SandboxEditorModal, onOpenSandboxModal } = useSandboxEditor({
appId: appDetail._id, appId: appDetail._id,
chatId chatId
}); });
...@@ -81,7 +85,7 @@ const ChatTest = ({ appForm, setRenderEdit, form2WorkflowFn }: Props) => { ...@@ -81,7 +85,7 @@ const ChatTest = ({ appForm, setRenderEdit, form2WorkflowFn }: Props) => {
{!isVariableVisible && <VariablePopover chatType={ChatTypeEnum.test} />} {!isVariableVisible && <VariablePopover chatType={ChatTypeEnum.test} />}
<Box flex={1} /> <Box flex={1} />
<SandboxEntryIcon size={'smSquare'} mr={2} /> <SandboxEntryIcon size={'smSquare'} mr={2} onOpen={onOpenSandboxModal} />
<MyTooltip label={t('common:core.chat.Restart')}> <MyTooltip label={t('common:core.chat.Restart')}>
<IconButton <IconButton
className="chat" className="chat"
......
...@@ -21,7 +21,7 @@ import { useContextSelector } from 'use-context-selector'; ...@@ -21,7 +21,7 @@ import { useContextSelector } from 'use-context-selector';
import ChatQuoteList from '@/pageComponents/chat/ChatQuoteList'; import ChatQuoteList from '@/pageComponents/chat/ChatQuoteList';
import { ChatTypeEnum } from '@/components/core/chat/ChatContainer/ChatBox/constants'; import { ChatTypeEnum } from '@/components/core/chat/ChatContainer/ChatBox/constants';
import { DetailLogsModalFeedbackTypeFilter } from './FeedbackTypeFilter'; import { DetailLogsModalFeedbackTypeFilter } from './FeedbackTypeFilter';
import { useSandboxEditor } from '@/pageComponents/chat/SandboxEditor/hook'; import { useSandboxEditor, useSandboxStatus } from '@/pageComponents/chat/SandboxEditor/hook';
import MyIcon from '@fastgpt/web/components/common/Icon'; import MyIcon from '@fastgpt/web/components/common/Icon';
const PluginRunBox = dynamic(() => import('@/components/core/chat/ChatContainer/PluginRunBox')); const PluginRunBox = dynamic(() => import('@/components/core/chat/ChatContainer/PluginRunBox'));
...@@ -87,11 +87,9 @@ const DetailLogsModal = ({ ...@@ -87,11 +87,9 @@ const DetailLogsModal = ({
const chatModels = chat?.app?.chatModels; const chatModels = chat?.app?.chatModels;
const isPlugin = chat?.app.type === AppTypeEnum.workflowTool; const isPlugin = chat?.app.type === AppTypeEnum.workflowTool;
// Sandbox state // Sandbox: Status Hook 负责网络同步,UI Hook 负责弹窗渲染
const { SandboxEditorModal, SandboxEntryIcon } = useSandboxEditor({ const { SandboxEntryIcon } = useSandboxStatus({ appId, chatId });
appId, const { SandboxEditorModal, onOpenSandboxModal } = useSandboxEditor({ appId, chatId });
chatId
});
return ( return (
<> <>
...@@ -176,7 +174,7 @@ const DetailLogsModal = ({ ...@@ -176,7 +174,7 @@ const DetailLogsModal = ({
</> </>
)} )}
<SandboxEntryIcon size={'smSquare'} mr={2} /> <SandboxEntryIcon size={'smSquare'} mr={2} onOpen={onOpenSandboxModal} />
<IconButton <IconButton
variant={'whiteBase'} variant={'whiteBase'}
size={'smSquare'} size={'smSquare'}
......
...@@ -24,7 +24,7 @@ import ChatQuoteList from '@/pageComponents/chat/ChatQuoteList'; ...@@ -24,7 +24,7 @@ import ChatQuoteList from '@/pageComponents/chat/ChatQuoteList';
import VariablePopover from '@/components/core/chat/ChatContainer/components/VariablePopover'; import VariablePopover from '@/components/core/chat/ChatContainer/components/VariablePopover';
import { useCopyData } from '@fastgpt/web/hooks/useCopyData'; import { useCopyData } from '@fastgpt/web/hooks/useCopyData';
import { ChatTypeEnum } from '@/components/core/chat/ChatContainer/ChatBox/constants'; import { ChatTypeEnum } from '@/components/core/chat/ChatContainer/ChatBox/constants';
import { useSandboxEditor } from '@/pageComponents/chat/SandboxEditor/hook'; import { useSandboxEditor, useSandboxStatus } from '@/pageComponents/chat/SandboxEditor/hook';
type Props = { type Props = {
isOpen: boolean; isOpen: boolean;
...@@ -54,8 +54,12 @@ const ChatTest = ({ isOpen, nodes = [], edges = [], onClose, chatId }: Props) => ...@@ -54,8 +54,12 @@ const ChatTest = ({ isOpen, nodes = [], edges = [], onClose, chatId }: Props) =>
const isVariableVisible = useContextSelector(ChatItemContext, (v) => v.isVariableVisible); const isVariableVisible = useContextSelector(ChatItemContext, (v) => v.isVariableVisible);
const chatRecords = useContextSelector(ChatRecordContext, (v) => v.chatRecords); const chatRecords = useContextSelector(ChatRecordContext, (v) => v.chatRecords);
// Sandbox state // Sandbox: Status Hook 负责网络同步,UI Hook 负责弹窗渲染
const { SandboxEditorModal, SandboxEntryIcon } = useSandboxEditor({ const { SandboxEntryIcon } = useSandboxStatus({
appId: appDetail._id,
chatId
});
const { SandboxEditorModal, onOpenSandboxModal } = useSandboxEditor({
appId: appDetail._id, appId: appDetail._id,
chatId chatId
}); });
...@@ -143,7 +147,7 @@ const ChatTest = ({ isOpen, nodes = [], edges = [], onClose, chatId }: Props) => ...@@ -143,7 +147,7 @@ const ChatTest = ({ isOpen, nodes = [], edges = [], onClose, chatId }: Props) =>
{!isVariableVisible && <VariablePopover chatType={ChatTypeEnum.test} />} {!isVariableVisible && <VariablePopover chatType={ChatTypeEnum.test} />}
<Box flex={1} /> <Box flex={1} />
<SandboxEntryIcon mr={2} /> <SandboxEntryIcon mr={2} onOpen={onOpenSandboxModal} />
<MyTooltip label={t('common:core.chat.Restart')}> <MyTooltip label={t('common:core.chat.Restart')}>
<IconButton <IconButton
mr={2} mr={2}
......
import { useCallback, useState } from 'react'; import { useCallback, useEffect, useMemo, useState, useRef } from 'react';
import SandboxEditorModal from '@/pageComponents/chat/SandboxEditor/modal'; import SandboxEditorModal from '@/pageComponents/chat/SandboxEditor/modal';
import type { IconButtonProps } from '@chakra-ui/react'; import type { IconButtonProps } from '@chakra-ui/react';
import { IconButton } from '@chakra-ui/react'; import { IconButton } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon'; import MyIcon from '@fastgpt/web/components/common/Icon';
import { checkSandboxExist } from './api'; import { checkSandboxExist } from './api';
import { useInterval } from 'ahooks';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip'; import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import type { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat'; import type { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
import { useContextSelector } from 'use-context-selector';
import { ChatRecordContext } from '@/web/core/chat/context/chatRecordContext';
import { addStatisticalDataToHistoryItem } from '@/global/core/chat/utils';
/**
* useSandboxEditor —— UI Hook
*
* 职责:仅负责渲染 SandboxEditorModal 弹窗及其开关逻辑。
*/
export const useSandboxEditor = ({ export const useSandboxEditor = ({
appId, appId,
chatId, chatId,
outLinkAuthData outLinkAuthData,
afterClose
}: { }: {
appId: string; appId: string;
chatId: string; chatId: string;
outLinkAuthData?: OutLinkChatAuthProps; outLinkAuthData?: OutLinkChatAuthProps;
afterClose?: () => void;
}) => { }) => {
const { t } = useTranslation();
// Sandbox state
const [sandboxModalOpen, setSandboxModalOpen] = useState(false); const [sandboxModalOpen, setSandboxModalOpen] = useState(false);
const [sandboxExists, setSandboxExists] = useState(false);
// 检查沙盒是否存在
const checkSandboxStatus = useCallback(async () => {
try {
const result = await checkSandboxExist({ appId, chatId, outLinkAuthData });
setSandboxExists(result.exists);
} catch (error) {
console.error('Failed to check sandbox status:', error);
}
}, [appId, chatId, outLinkAuthData]);
// 组件挂载时检查
useInterval(checkSandboxStatus, 10000, {
immediate: true
});
const onOpenSandboxModal = useCallback(() => { const onOpenSandboxModal = useCallback(() => {
setSandboxModalOpen(true); setSandboxModalOpen(true);
...@@ -44,11 +35,10 @@ export const useSandboxEditor = ({ ...@@ -44,11 +35,10 @@ export const useSandboxEditor = ({
const onCloseSandboxModal = useCallback(() => { const onCloseSandboxModal = useCallback(() => {
setSandboxModalOpen(false); setSandboxModalOpen(false);
// 关闭后重新检查状态 afterClose?.();
checkSandboxStatus(); }, [afterClose]);
}, [checkSandboxStatus]);
const Dom = useCallback(() => { const SandboxEditorModalDom = useCallback(() => {
return sandboxModalOpen ? ( return sandboxModalOpen ? (
<SandboxEditorModal <SandboxEditorModal
onClose={onCloseSandboxModal} onClose={onCloseSandboxModal}
...@@ -59,9 +49,75 @@ export const useSandboxEditor = ({ ...@@ -59,9 +49,75 @@ export const useSandboxEditor = ({
) : null; ) : null;
}, [sandboxModalOpen, onCloseSandboxModal, appId, chatId, outLinkAuthData]); }, [sandboxModalOpen, onCloseSandboxModal, appId, chatId, outLinkAuthData]);
return {
SandboxEditorModal: SandboxEditorModalDom,
onOpenSandboxModal,
onCloseSandboxModal
};
};
/**
* useSandboxStatus —— Status Hook
*
* 职责:负责 checkSandboxExist 的网络同步及 SandboxEntryIcon 的显示控制。
* 同步模式:
* 1. 历史记录(ChatRecordContext):useMemo 派生,无副作用。
* 2. chatId 切换:渲染周期利用 useRef 确认 ID 变化并同步重置状态,防止 UI 闪烁。
* 3. 网络请求:单一 useEffect,在参数变化时触发 1 次。
*/
export const useSandboxStatus = ({
appId,
chatId,
outLinkAuthData
}: {
appId: string;
chatId: string;
outLinkAuthData?: OutLinkChatAuthProps;
}) => {
const { t } = useTranslation();
const [apiSandboxExists, setApiSandboxExists] = useState(false);
const lastChatIdRef = useRef(chatId);
if (lastChatIdRef.current !== chatId) {
lastChatIdRef.current = chatId;
setApiSandboxExists(false);
}
const chatRecords = useContextSelector(ChatRecordContext, (v) => {
return v.chatRecords;
});
const isChatRecordsLoaded = useContextSelector(ChatRecordContext, (v) => v.isChatRecordsLoaded);
const hasSandboxInHistory = useMemo(() => {
if (!isChatRecordsLoaded) return false;
return chatRecords.some((record) => {
const enriched = addStatisticalDataToHistoryItem(record);
return enriched.useAgentSandbox === true;
});
}, [chatRecords, isChatRecordsLoaded]);
useEffect(() => {
if (!chatId) return;
let cancelled = false;
checkSandboxExist({ appId, chatId, outLinkAuthData })
.then((result) => {
if (!cancelled) setApiSandboxExists(result.exists);
})
.catch((error) => {
console.error('Failed to check sandbox status:', error);
});
return () => {
cancelled = true;
};
}, [appId, chatId]);
const sandboxExists = hasSandboxInHistory || apiSandboxExists;
const SandboxEntryIcon = useCallback( const SandboxEntryIcon = useCallback(
(props: Omit<IconButtonProps, 'name' | 'onClick' | 'aria-label'>) => { ({
// 只有沙盒存在时才显示图标 onOpen,
...props
}: Omit<IconButtonProps, 'name' | 'onClick' | 'aria-label'> & { onOpen: () => void }) => {
if (!sandboxExists) return null; if (!sandboxExists) return null;
return ( return (
...@@ -70,23 +126,19 @@ export const useSandboxEditor = ({ ...@@ -70,23 +126,19 @@ export const useSandboxEditor = ({
variant={'whiteBase'} variant={'whiteBase'}
size={'smSquare'} size={'smSquare'}
icon={<MyIcon name={'core/app/sandbox/file'} w={'16px'} />} icon={<MyIcon name={'core/app/sandbox/file'} w={'16px'} />}
onClick={onOpenSandboxModal} onClick={onOpen}
{...props} {...props}
aria-label="Sandbox Entry" aria-label="Sandbox Entry"
/> />
</MyTooltip> </MyTooltip>
); );
}, },
[sandboxExists, t, onOpenSandboxModal] [sandboxExists, t]
); );
return { return {
sandboxExists, sandboxExists,
setSandboxExists, setSandboxExists: setApiSandboxExists,
checkSandboxStatus, SandboxEntryIcon
SandboxEntryIcon,
SandboxEditorModal: Dom,
onOpenSandboxModal,
onCloseSandboxModal
}; };
}; };
...@@ -8,7 +8,7 @@ import MyMenu from '@fastgpt/web/components/common/MyMenu'; ...@@ -8,7 +8,7 @@ import MyMenu from '@fastgpt/web/components/common/MyMenu';
import { useContextSelector } from 'use-context-selector'; import { useContextSelector } from 'use-context-selector';
import { ChatContext } from '@/web/core/chat/context/chatContext'; import { ChatContext } from '@/web/core/chat/context/chatContext';
import { ChatItemContext } from '@/web/core/chat/context/chatItemContext'; import { ChatItemContext } from '@/web/core/chat/context/chatItemContext';
import { useSandboxEditor } from './SandboxEditor/hook'; import { useSandboxEditor, useSandboxStatus } from './SandboxEditor/hook';
import { useChatStore } from '@/web/core/chat/context/useChatStore'; import { useChatStore } from '@/web/core/chat/context/useChatStore';
import { useSystem } from '@fastgpt/web/hooks/useSystem'; import { useSystem } from '@fastgpt/web/hooks/useSystem';
...@@ -25,16 +25,17 @@ const ToolMenu = ({ ...@@ -25,16 +25,17 @@ const ToolMenu = ({
const onChangeChatId = useContextSelector(ChatContext, (v) => v.onChangeChatId); const onChangeChatId = useContextSelector(ChatContext, (v) => v.onChangeChatId);
const chatData = useContextSelector(ChatItemContext, (v) => v.chatBoxData); const chatData = useContextSelector(ChatItemContext, (v) => v.chatBoxData);
const { chatId, appId, setChatId, outLinkAuthData } = useChatStore(); const { chatId, outLinkAuthData } = useChatStore();
// Sandbox state // Status Hook: 顶层单例,负责网络同步与入口图标显示
const { const { sandboxExists, setSandboxExists, SandboxEntryIcon } = useSandboxStatus({
SandboxEditorModal, appId: chatData.appId,
SandboxEntryIcon, chatId,
setSandboxExists, outLinkAuthData
sandboxExists, });
onOpenSandboxModal
} = useSandboxEditor({ // UI Hook: 负责弹窗渲染
const { SandboxEditorModal, onOpenSandboxModal } = useSandboxEditor({
appId: chatData.appId, appId: chatData.appId,
chatId, chatId,
outLinkAuthData outLinkAuthData
...@@ -42,7 +43,7 @@ const ToolMenu = ({ ...@@ -42,7 +43,7 @@ const ToolMenu = ({
return ( return (
<> <>
{isPc && <SandboxEntryIcon />} {isPc && <SandboxEntryIcon onOpen={onOpenSandboxModal} />}
<MyMenu <MyMenu
Button={ Button={
<Box transform={reserveSpace ? 'translateX(-32px)' : 'none'}> <Box transform={reserveSpace ? 'translateX(-32px)' : 'none'}>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment