-
Notifications
You must be signed in to change notification settings - Fork 7k
refactor(chat): optimize sandbox status logic and decouple UI/Status hooks #6713
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,54 +1,44 @@ | ||
| 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); | ||
| }, []); | ||
|
|
||
| const onCloseSandboxModal = useCallback(() => { | ||
c121914yu marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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, | ||
c121914yu marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| outLinkAuthData | ||
| }: { | ||
| appId: string; | ||
| chatId: string; | ||
| outLinkAuthData?: OutLinkChatAuthProps; | ||
| }) => { | ||
| const { t } = useTranslation(); | ||
| const [apiSandboxExists, setApiSandboxExists] = useState(false); | ||
| const lastChatIdRef = useRef(chatId); | ||
|
|
||
c121914yu marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if (lastChatIdRef.current !== chatId) { | ||
| lastChatIdRef.current = chatId; | ||
| setApiSandboxExists(false); | ||
| } | ||
|
|
||
| const chatRecords = useContextSelector(ChatRecordContext, (v) => { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟢 优化建议 — 网络请求从轮询改为单次的取舍 原来的实现使用 这个改动减少了不必要的网络请求,但也意味着:如果用户在聊天过程中 AI 创建了新的 sandbox(流式响应结束后),状态不会自动更新,用户需要切换聊天再切回来才能看到 sandbox 图标。
|
||
| return v.chatRecords; | ||
| }); | ||
| const isChatRecordsLoaded = useContextSelector(ChatRecordContext, (v) => v.isChatRecordsLoaded); | ||
|
|
||
c121914yu marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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; | ||
DigHuang marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| checkSandboxExist({ appId, chatId, outLinkAuthData }) | ||
| .then((result) => { | ||
| if (!cancelled) setApiSandboxExists(result.exists); | ||
| }) | ||
| .catch((error) => { | ||
| console.error('Failed to check sandbox status:', error); | ||
c121914yu marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| }); | ||
| 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 | ||
| }; | ||
| }; | ||
Uh oh!
There was an error while loading. Please reload this page.