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 = {
citeCollectionIds?: string[];
/**
* @deprecated 不再存储在 chatItemSchema 里,分别存储到 chatItemResponseSchema
* 不再存储在 chatItemSchema 里,分别存储到 chatItemResponseSchema
*/
[DispatchNodeResponseKeyEnum.nodeResponse]?: ChatHistoryItemResType[];
};
......
......@@ -24,7 +24,7 @@ import type { HelperBotRefType } from '@/components/core/chat/HelperBot/context'
import { HelperBotTypeEnum } from '@fastgpt/global/core/chat/helperBot/type';
import { loadGeneratedTools } from './utils';
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 = {
appForm: AppFormEditFormType;
......@@ -50,8 +50,12 @@ const ChatTest = ({ appForm, setAppForm, setRenderEdit, form2WorkflowFn }: Props
edges: appDetail.edges || []
});
// Sandbox state
const { SandboxEditorModal, SandboxEntryIcon } = useSandboxEditor({
// Sandbox: Status Hook 负责网络同步,UI Hook 负责弹窗渲染
const { SandboxEntryIcon } = useSandboxStatus({
appId: appDetail._id,
chatId
});
const { SandboxEditorModal, onOpenSandboxModal } = useSandboxEditor({
appId: appDetail._id,
chatId
});
......@@ -126,7 +130,7 @@ const ChatTest = ({ appForm, setAppForm, setRenderEdit, form2WorkflowFn }: Props
)}
<Box flex={1} />
<SandboxEntryIcon size={'smSquare'} mr={2} />
<SandboxEntryIcon size={'smSquare'} mr={2} onOpen={onOpenSandboxModal} />
<MyTooltip label={t('common:core.chat.Restart')}>
<IconButton
className="chat"
......
......@@ -18,7 +18,7 @@ import ChatQuoteList from '@/pageComponents/chat/ChatQuoteList';
import VariablePopover from '@/components/core/chat/ChatContainer/components/VariablePopover';
import { ChatTypeEnum } from '@/components/core/chat/ChatContainer/ChatBox/constants';
import type { Form2WorkflowFnType } from '../FormComponent/type';
import { useSandboxEditor } from '@/pageComponents/chat/SandboxEditor/hook';
import { useSandboxEditor, useSandboxStatus } from '@/pageComponents/chat/SandboxEditor/hook';
type Props = {
appForm: AppFormEditFormType;
......@@ -40,8 +40,12 @@ const ChatTest = ({ appForm, setRenderEdit, form2WorkflowFn }: Props) => {
edges: appDetail.edges || []
});
// Sandbox state
const { SandboxEditorModal, SandboxEntryIcon, setSandboxExists } = useSandboxEditor({
// Sandbox: Status Hook 负责网络同步,UI Hook 负责弹窗渲染
const { SandboxEntryIcon, setSandboxExists } = useSandboxStatus({
appId: appDetail._id,
chatId
});
const { SandboxEditorModal, onOpenSandboxModal } = useSandboxEditor({
appId: appDetail._id,
chatId
});
......@@ -81,7 +85,7 @@ const ChatTest = ({ appForm, setRenderEdit, form2WorkflowFn }: Props) => {
{!isVariableVisible && <VariablePopover chatType={ChatTypeEnum.test} />}
<Box flex={1} />
<SandboxEntryIcon size={'smSquare'} mr={2} />
<SandboxEntryIcon size={'smSquare'} mr={2} onOpen={onOpenSandboxModal} />
<MyTooltip label={t('common:core.chat.Restart')}>
<IconButton
className="chat"
......
......@@ -21,7 +21,7 @@ import { useContextSelector } from 'use-context-selector';
import ChatQuoteList from '@/pageComponents/chat/ChatQuoteList';
import { ChatTypeEnum } from '@/components/core/chat/ChatContainer/ChatBox/constants';
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';
const PluginRunBox = dynamic(() => import('@/components/core/chat/ChatContainer/PluginRunBox'));
......@@ -87,11 +87,9 @@ const DetailLogsModal = ({
const chatModels = chat?.app?.chatModels;
const isPlugin = chat?.app.type === AppTypeEnum.workflowTool;
// Sandbox state
const { SandboxEditorModal, SandboxEntryIcon } = useSandboxEditor({
appId,
chatId
});
// Sandbox: Status Hook 负责网络同步,UI Hook 负责弹窗渲染
const { SandboxEntryIcon } = useSandboxStatus({ appId, chatId });
const { SandboxEditorModal, onOpenSandboxModal } = useSandboxEditor({ appId, chatId });
return (
<>
......@@ -176,7 +174,7 @@ const DetailLogsModal = ({
</>
)}
<SandboxEntryIcon size={'smSquare'} mr={2} />
<SandboxEntryIcon size={'smSquare'} mr={2} onOpen={onOpenSandboxModal} />
<IconButton
variant={'whiteBase'}
size={'smSquare'}
......
......@@ -24,7 +24,7 @@ import ChatQuoteList from '@/pageComponents/chat/ChatQuoteList';
import VariablePopover from '@/components/core/chat/ChatContainer/components/VariablePopover';
import { useCopyData } from '@fastgpt/web/hooks/useCopyData';
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 = {
isOpen: boolean;
......@@ -54,8 +54,12 @@ const ChatTest = ({ isOpen, nodes = [], edges = [], onClose, chatId }: Props) =>
const isVariableVisible = useContextSelector(ChatItemContext, (v) => v.isVariableVisible);
const chatRecords = useContextSelector(ChatRecordContext, (v) => v.chatRecords);
// Sandbox state
const { SandboxEditorModal, SandboxEntryIcon } = useSandboxEditor({
// Sandbox: Status Hook 负责网络同步,UI Hook 负责弹窗渲染
const { SandboxEntryIcon } = useSandboxStatus({
appId: appDetail._id,
chatId
});
const { SandboxEditorModal, onOpenSandboxModal } = useSandboxEditor({
appId: appDetail._id,
chatId
});
......@@ -143,7 +147,7 @@ const ChatTest = ({ isOpen, nodes = [], edges = [], onClose, chatId }: Props) =>
{!isVariableVisible && <VariablePopover chatType={ChatTypeEnum.test} />}
<Box flex={1} />
<SandboxEntryIcon mr={2} />
<SandboxEntryIcon mr={2} onOpen={onOpenSandboxModal} />
<MyTooltip label={t('common:core.chat.Restart')}>
<IconButton
mr={2}
......
import { useCallback, useState } from 'react';
import { useCallback, useEffect, useMemo, useState, useRef } from 'react';
import SandboxEditorModal from '@/pageComponents/chat/SandboxEditor/modal';
import type { IconButtonProps } from '@chakra-ui/react';
import { IconButton } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { checkSandboxExist } from './api';
import { useInterval } from 'ahooks';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { useTranslation } from 'next-i18next';
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 = ({
appId,
chatId,
outLinkAuthData
outLinkAuthData,
afterClose
}: {
appId: string;
chatId: string;
outLinkAuthData?: OutLinkChatAuthProps;
afterClose?: () => void;
}) => {
const { t } = useTranslation();
// Sandbox state
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(() => {
setSandboxModalOpen(true);
......@@ -44,11 +35,10 @@ export const useSandboxEditor = ({
const onCloseSandboxModal = useCallback(() => {
setSandboxModalOpen(false);
// 关闭后重新检查状态
checkSandboxStatus();
}, [checkSandboxStatus]);
afterClose?.();
}, [afterClose]);
const Dom = useCallback(() => {
const SandboxEditorModalDom = useCallback(() => {
return sandboxModalOpen ? (
<SandboxEditorModal
onClose={onCloseSandboxModal}
......@@ -59,9 +49,75 @@ export const useSandboxEditor = ({
) : null;
}, [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(
(props: Omit<IconButtonProps, 'name' | 'onClick' | 'aria-label'>) => {
// 只有沙盒存在时才显示图标
({
onOpen,
...props
}: Omit<IconButtonProps, 'name' | 'onClick' | 'aria-label'> & { onOpen: () => void }) => {
if (!sandboxExists) return null;
return (
......@@ -70,23 +126,19 @@ export const useSandboxEditor = ({
variant={'whiteBase'}
size={'smSquare'}
icon={<MyIcon name={'core/app/sandbox/file'} w={'16px'} />}
onClick={onOpenSandboxModal}
onClick={onOpen}
{...props}
aria-label="Sandbox Entry"
/>
</MyTooltip>
);
},
[sandboxExists, t, onOpenSandboxModal]
[sandboxExists, t]
);
return {
sandboxExists,
setSandboxExists,
checkSandboxStatus,
SandboxEntryIcon,
SandboxEditorModal: Dom,
onOpenSandboxModal,
onCloseSandboxModal
setSandboxExists: setApiSandboxExists,
SandboxEntryIcon
};
};
......@@ -8,7 +8,7 @@ import MyMenu from '@fastgpt/web/components/common/MyMenu';
import { useContextSelector } from 'use-context-selector';
import { ChatContext } from '@/web/core/chat/context/chatContext';
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 { useSystem } from '@fastgpt/web/hooks/useSystem';
......@@ -25,16 +25,17 @@ const ToolMenu = ({
const onChangeChatId = useContextSelector(ChatContext, (v) => v.onChangeChatId);
const chatData = useContextSelector(ChatItemContext, (v) => v.chatBoxData);
const { chatId, appId, setChatId, outLinkAuthData } = useChatStore();
const { chatId, outLinkAuthData } = useChatStore();
// Sandbox state
const {
SandboxEditorModal,
SandboxEntryIcon,
setSandboxExists,
sandboxExists,
onOpenSandboxModal
} = useSandboxEditor({
// Status Hook: 顶层单例,负责网络同步与入口图标显示
const { sandboxExists, setSandboxExists, SandboxEntryIcon } = useSandboxStatus({
appId: chatData.appId,
chatId,
outLinkAuthData
});
// UI Hook: 负责弹窗渲染
const { SandboxEditorModal, onOpenSandboxModal } = useSandboxEditor({
appId: chatData.appId,
chatId,
outLinkAuthData
......@@ -42,7 +43,7 @@ const ToolMenu = ({
return (
<>
{isPc && <SandboxEntryIcon />}
{isPc && <SandboxEntryIcon onOpen={onOpenSandboxModal} />}
<MyMenu
Button={
<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