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:
Archer
2024-09-24 17:13:32 +08:00
committed by GitHub
parent f4d4d6516c
commit 434c03c955
46 changed files with 827 additions and 422 deletions

View File

@@ -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,

View File

@@ -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';

View File

@@ -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,

View File

@@ -40,4 +40,5 @@ export type SendPromptFnType = (
export type ComponentRef = {
restartChat: () => void;
scrollToBottom: (behavior?: 'smooth' | 'auto') => void;
};

View File

@@ -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
};
};