Record scroll test (#2783)
* perf: history add scrollList (#2696) * perf: chatHistorySlider add virtualList * perf: chat records add scrollList * delete console * perf: ScrollData add ref props * 优化代码 * optimize code && add line breaks * add total records display * finish test * perf: ScrollComponent load data * perf: Scroll components load * perf: scroll code --------- Co-authored-by: papapatrick <109422393+Patrickill@users.noreply.github.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { Box, Flex } from '@chakra-ui/react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useLoading } from '@fastgpt/web/hooks/useLoading';
|
||||
@@ -12,7 +12,7 @@ import { useI18nLng } from '@fastgpt/web/hooks/useI18n';
|
||||
import Auth from './auth';
|
||||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||
import { useMount } from 'ahooks';
|
||||
import { watchWindowHidden } from '@/web/common/system/utils';
|
||||
|
||||
const Navbar = dynamic(() => import('./navbar'));
|
||||
const NavbarPhone = dynamic(() => import('./navbarPhone'));
|
||||
const UpdateInviteModal = dynamic(() => import('@/components/support/user/team/UpdateInviteModal'));
|
||||
@@ -70,14 +70,6 @@ const Layout = ({ children }: { children: JSX.Element }) => {
|
||||
setUserDefaultLng();
|
||||
});
|
||||
|
||||
// Add global listener
|
||||
useEffect(() => {
|
||||
document.addEventListener('visibilitychange', watchWindowHidden);
|
||||
return () => {
|
||||
document.removeEventListener('visibilitychange', watchWindowHidden);
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box h={'100%'} bg={'myGray.100'}>
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
putChatInputGuide
|
||||
} from '@/web/core/chat/inputGuide/api';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
|
||||
import { useVirtualScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
|
||||
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
|
||||
@@ -204,7 +204,7 @@ const LexiconConfigModal = ({ appId, onClose }: { appId: string; onClose: () =>
|
||||
isLoading: isRequesting,
|
||||
fetchData,
|
||||
scroll2Top
|
||||
} = useScrollPagination(getChatInputGuideList, {
|
||||
} = useVirtualScrollPagination(getChatInputGuideList, {
|
||||
refreshDeps: [searchKey],
|
||||
// debounceWait: 300,
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
defaultWhisperConfig
|
||||
} from '@fastgpt/global/core/app/constants';
|
||||
import { createContext } from 'use-context-selector';
|
||||
import { FieldValues, UseFormReturn } from 'react-hook-form';
|
||||
import { UseFormReturn } from 'react-hook-form';
|
||||
import { VariableInputEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import { getChatResData } from '@/web/core/chat/api';
|
||||
import { ChatBoxInputFormType } from './type';
|
||||
|
||||
@@ -16,7 +16,7 @@ import type {
|
||||
} from '@fastgpt/global/core/chat/type.d';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { getErrText } from '@fastgpt/global/common/error/utils';
|
||||
import { Box, Flex, Checkbox } from '@chakra-ui/react';
|
||||
import { Box, Flex, Checkbox, BoxProps } from '@chakra-ui/react';
|
||||
import { EventNameEnum, eventBus } from '@/web/common/utils/eventbus';
|
||||
import { chats2GPTMessages } from '@fastgpt/global/core/chat/adapt';
|
||||
import { useForm } from 'react-hook-form';
|
||||
@@ -44,7 +44,11 @@ import ChatInput from './Input/ChatInput';
|
||||
import ChatBoxDivider from '../../Divider';
|
||||
import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
|
||||
import { getNanoid } from '@fastgpt/global/common/string/tools';
|
||||
import { ChatItemValueTypeEnum, ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
|
||||
import {
|
||||
ChatItemValueTypeEnum,
|
||||
ChatRoleEnum,
|
||||
ChatStatusEnum
|
||||
} from '@fastgpt/global/core/chat/constants';
|
||||
import {
|
||||
checkIsInteractiveByHistories,
|
||||
formatChatValue2InputType,
|
||||
@@ -86,7 +90,13 @@ type Props = OutLinkChatAuthProps &
|
||||
userAvatar?: string;
|
||||
active?: boolean; // can use
|
||||
appId: string;
|
||||
|
||||
ScrollData: ({
|
||||
children,
|
||||
...props
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
ScrollContainerRef?: React.RefObject<HTMLDivElement>;
|
||||
} & BoxProps) => React.JSX.Element;
|
||||
// not chat test params
|
||||
|
||||
onStartChat?: (e: StartChatFnProps) => Promise<
|
||||
@@ -113,7 +123,8 @@ const ChatBox = (
|
||||
teamId,
|
||||
teamToken,
|
||||
onStartChat,
|
||||
onDelMessage
|
||||
onDelMessage,
|
||||
ScrollData
|
||||
}: Props,
|
||||
ref: ForwardedRef<ComponentRef>
|
||||
) => {
|
||||
@@ -171,7 +182,7 @@ const ChatBox = (
|
||||
const chatStarted = chatStartedWatch || chatHistories.length > 0 || variableList.length === 0;
|
||||
|
||||
// 滚动到底部
|
||||
const scrollToBottom = useCallback((behavior: 'smooth' | 'auto' = 'smooth', delay = 0) => {
|
||||
const scrollToBottom = useMemoizedFn((behavior: 'smooth' | 'auto' = 'smooth', delay = 0) => {
|
||||
setTimeout(() => {
|
||||
if (!ChatBoxRef.current) {
|
||||
setTimeout(() => {
|
||||
@@ -184,7 +195,7 @@ const ChatBox = (
|
||||
});
|
||||
}
|
||||
}, delay);
|
||||
}, []);
|
||||
});
|
||||
|
||||
// 聊天信息生成中……获取当前滚动条位置,判断是否需要滚动到底部
|
||||
const { run: generatingScroll } = useThrottleFn(
|
||||
@@ -201,7 +212,7 @@ const ChatBox = (
|
||||
}
|
||||
);
|
||||
|
||||
const generatingMessage = useCallback(
|
||||
const generatingMessage = useMemoizedFn(
|
||||
({
|
||||
event,
|
||||
text = '',
|
||||
@@ -311,27 +322,23 @@ const ChatBox = (
|
||||
})
|
||||
);
|
||||
generatingScroll();
|
||||
},
|
||||
[generatingScroll, setChatHistories, splitText2Audio, variablesForm]
|
||||
}
|
||||
);
|
||||
|
||||
// 重置输入内容
|
||||
const resetInputVal = useCallback(
|
||||
({ text = '', files = [] }: ChatBoxInputType) => {
|
||||
if (!TextareaDom.current) return;
|
||||
setValue('files', files);
|
||||
setValue('input', text);
|
||||
const resetInputVal = useMemoizedFn(({ text = '', files = [] }: ChatBoxInputType) => {
|
||||
if (!TextareaDom.current) return;
|
||||
setValue('files', files);
|
||||
setValue('input', text);
|
||||
|
||||
setTimeout(() => {
|
||||
/* 回到最小高度 */
|
||||
if (TextareaDom.current) {
|
||||
TextareaDom.current.style.height =
|
||||
text === '' ? textareaMinH : `${TextareaDom.current.scrollHeight}px`;
|
||||
}
|
||||
}, 100);
|
||||
},
|
||||
[setValue]
|
||||
);
|
||||
setTimeout(() => {
|
||||
/* 回到最小高度 */
|
||||
if (TextareaDom.current) {
|
||||
TextareaDom.current.style.height =
|
||||
text === '' ? textareaMinH : `${TextareaDom.current.scrollHeight}px`;
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
|
||||
// create question guide
|
||||
const createQuestionGuide = useCallback(
|
||||
@@ -363,11 +370,11 @@ const ChatBox = (
|
||||
);
|
||||
|
||||
/* Abort chat completions, questionGuide */
|
||||
const abortRequest = useCallback(() => {
|
||||
const abortRequest = useMemoizedFn(() => {
|
||||
chatController.current?.abort('stop');
|
||||
questionGuideController.current?.abort('stop');
|
||||
pluginController.current?.abort('stop');
|
||||
}, []);
|
||||
});
|
||||
|
||||
/**
|
||||
* user confirm send prompt
|
||||
@@ -445,7 +452,7 @@ const ChatBox = (
|
||||
]
|
||||
: [])
|
||||
] as UserChatItemValueItemType[],
|
||||
status: 'finish'
|
||||
status: ChatStatusEnum.finish
|
||||
},
|
||||
// 普通 chat 模式,需要增加一个 AI 来接收响应消息
|
||||
{
|
||||
@@ -459,7 +466,7 @@ const ChatBox = (
|
||||
}
|
||||
}
|
||||
],
|
||||
status: 'loading'
|
||||
status: ChatStatusEnum.loading
|
||||
}
|
||||
];
|
||||
|
||||
@@ -506,7 +513,7 @@ const ChatBox = (
|
||||
if (index !== state.length - 1) return item;
|
||||
return {
|
||||
...item,
|
||||
status: 'finish',
|
||||
status: ChatStatusEnum.finish,
|
||||
responseData: item.responseData
|
||||
? [...item.responseData, ...responseData]
|
||||
: responseData
|
||||
@@ -548,7 +555,7 @@ const ChatBox = (
|
||||
if (index !== state.length - 1) return item;
|
||||
return {
|
||||
...item,
|
||||
status: 'finish'
|
||||
status: ChatStatusEnum.finish
|
||||
};
|
||||
})
|
||||
);
|
||||
@@ -806,7 +813,7 @@ const ChatBox = (
|
||||
if (!chatContent) return;
|
||||
|
||||
return {
|
||||
status: chatContent.status || 'loading',
|
||||
status: chatContent.status || ChatStatusEnum.loading,
|
||||
name: t(chatContent.moduleName || ('' as any)) || t('common:common.Loading')
|
||||
};
|
||||
}, [chatHistories, isChatting, t]);
|
||||
@@ -854,14 +861,26 @@ const ChatBox = (
|
||||
useImperativeHandle(ref, () => ({
|
||||
restartChat() {
|
||||
abortRequest();
|
||||
|
||||
setChatHistories([]);
|
||||
setValue('chatStarted', false);
|
||||
scrollToBottom('smooth', 500);
|
||||
},
|
||||
scrollToBottom(behavior = 'auto') {
|
||||
scrollToBottom(behavior, 500);
|
||||
}
|
||||
}));
|
||||
|
||||
const RenderRecords = useMemo(() => {
|
||||
return (
|
||||
<Box ref={ChatBoxRef} flex={'1 0 0'} h={0} w={'100%'} overflow={'overlay'} px={[4, 0]} pb={3}>
|
||||
<ScrollData
|
||||
ScrollContainerRef={ChatBoxRef}
|
||||
flex={'1 0 0'}
|
||||
h={0}
|
||||
w={'100%'}
|
||||
overflow={'overlay'}
|
||||
px={[4, 0]}
|
||||
pb={3}
|
||||
>
|
||||
<Box id="chat-container" maxW={['100%', '92%']} h={'100%'} mx={'auto'}>
|
||||
{showEmpty && <Empty />}
|
||||
{!!welcomeText && <WelcomeBox welcomeText={welcomeText} />}
|
||||
@@ -957,9 +976,10 @@ const ChatBox = (
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</ScrollData>
|
||||
);
|
||||
}, [
|
||||
ScrollData,
|
||||
appAvatar,
|
||||
chatForm,
|
||||
chatHistories,
|
||||
|
||||
@@ -40,4 +40,5 @@ export type SendPromptFnType = (
|
||||
|
||||
export type ComponentRef = {
|
||||
restartChat: () => void;
|
||||
scrollToBottom: (behavior?: 'smooth' | 'auto') => void;
|
||||
};
|
||||
|
||||
@@ -8,20 +8,23 @@ import {
|
||||
SendPromptFnType
|
||||
} from './ChatBox/type';
|
||||
import { eventBus, EventNameEnum } from '@/web/common/utils/eventbus';
|
||||
import { getChatRecords } from '@/web/core/chat/api';
|
||||
import { ChatStatusEnum } from '@fastgpt/global/core/chat/constants';
|
||||
import { getNanoid } from '@fastgpt/global/common/string/tools';
|
||||
import { GetChatRecordsProps } from '@/global/core/chat/api';
|
||||
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
|
||||
import { PaginationResponse } from '../../../../../../../packages/web/common/fetch/type';
|
||||
import type { getPaginationRecordsBody } from '@/pages/api/core/chat/getPaginationRecords';
|
||||
|
||||
export const useChat = () => {
|
||||
const ChatBoxRef = useRef<ChatComponentRef>(null);
|
||||
|
||||
const [chatRecords, setChatRecords] = useState<ChatSiteItemType[]>([]);
|
||||
const variablesForm = useForm<ChatBoxInputFormType>();
|
||||
// plugin
|
||||
const [pluginRunTab, setPluginRunTab] = useState<PluginRunBoxTabEnum>(PluginRunBoxTabEnum.input);
|
||||
|
||||
const resetChatRecords = useCallback(
|
||||
(props?: { records?: ChatSiteItemType[]; variables?: Record<string, any> }) => {
|
||||
const { records = [], variables = {} } = props || {};
|
||||
|
||||
setChatRecords(records);
|
||||
const resetVariables = useCallback(
|
||||
(props?: { variables?: Record<string, any> }) => {
|
||||
const { variables = {} } = props || {};
|
||||
|
||||
// Reset to empty input
|
||||
const data = variablesForm.getValues();
|
||||
@@ -33,20 +36,11 @@ export const useChat = () => {
|
||||
...data,
|
||||
...variables
|
||||
});
|
||||
|
||||
setTimeout(
|
||||
() => {
|
||||
ChatBoxRef.current?.restartChat?.();
|
||||
},
|
||||
ChatBoxRef.current?.restartChat ? 0 : 500
|
||||
);
|
||||
},
|
||||
[variablesForm, setChatRecords]
|
||||
[variablesForm]
|
||||
);
|
||||
|
||||
const clearChatRecords = useCallback(() => {
|
||||
setChatRecords([]);
|
||||
|
||||
const data = variablesForm.getValues();
|
||||
for (const key in data) {
|
||||
variablesForm.setValue(key, '');
|
||||
@@ -55,15 +49,47 @@ export const useChat = () => {
|
||||
ChatBoxRef.current?.restartChat?.();
|
||||
}, [variablesForm]);
|
||||
|
||||
const useChatScrollData = useCallback((params: GetChatRecordsProps) => {
|
||||
return useScrollPagination(
|
||||
async (data: getPaginationRecordsBody): Promise<PaginationResponse<ChatSiteItemType>> => {
|
||||
const res = await getChatRecords(data);
|
||||
|
||||
// First load scroll to bottom
|
||||
if (data.offset === 0) {
|
||||
function scrollToBottom() {
|
||||
requestAnimationFrame(
|
||||
ChatBoxRef?.current ? () => ChatBoxRef?.current?.scrollToBottom?.() : scrollToBottom
|
||||
);
|
||||
}
|
||||
scrollToBottom();
|
||||
}
|
||||
|
||||
return {
|
||||
...res,
|
||||
list: res.list.map((item) => ({
|
||||
...item,
|
||||
dataId: item.dataId || getNanoid(),
|
||||
status: ChatStatusEnum.finish
|
||||
}))
|
||||
};
|
||||
},
|
||||
{
|
||||
pageSize: 10,
|
||||
refreshDeps: [params],
|
||||
params,
|
||||
scrollLoadType: 'top'
|
||||
}
|
||||
);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
ChatBoxRef,
|
||||
chatRecords,
|
||||
setChatRecords,
|
||||
variablesForm,
|
||||
pluginRunTab,
|
||||
setPluginRunTab,
|
||||
clearChatRecords,
|
||||
resetChatRecords
|
||||
resetVariables,
|
||||
useChatScrollData
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user