diff --git a/packages/web/hooks/usePagination.tsx b/packages/web/hooks/usePagination.tsx index 26e467c3d2c8..adf00703c068 100644 --- a/packages/web/hooks/usePagination.tsx +++ b/packages/web/hooks/usePagination.tsx @@ -1,13 +1,17 @@ -import { useRef, useState, useCallback, useMemo, useEffect } from 'react'; -import { IconButton, Flex, Box, Input } from '@chakra-ui/react'; +import { useRef, useState, useCallback, useMemo } from 'react'; +import { IconButton, Flex, Box, Input, BoxProps } from '@chakra-ui/react'; import { ArrowBackIcon, ArrowForwardIcon } from '@chakra-ui/icons'; -import { useMutation } from '@tanstack/react-query'; import { useTranslation } from 'next-i18next'; -import { throttle } from 'lodash'; import { useToast } from './useToast'; import { getErrText } from '@fastgpt/global/common/error/utils'; - -const thresholdVal = 100; +import { + useBoolean, + useLockFn, + useMemoizedFn, + useRequest, + useScroll, + useThrottleEffect +} from 'ahooks'; type PagingData = { pageNum: number; @@ -23,7 +27,7 @@ export function usePagination({ defaultRequest = true, type = 'button', onChange, - elementRef + refreshDeps }: { api: (data: any) => Promise>; pageSize?: number; @@ -31,46 +35,59 @@ export function usePagination({ defaultRequest?: boolean; type?: 'button' | 'scroll'; onChange?: (pageNum: number) => void; - elementRef?: React.RefObject; + refreshDeps?: any[]; }) { const { toast } = useToast(); const { t } = useTranslation(); const [pageNum, setPageNum] = useState(1); - const pageNumRef = useRef(pageNum); - pageNumRef.current = pageNum; + + const ScrollContainerRef = useRef(null); + + const noMore = useRef(false); + + const [isLoading, { setTrue, setFalse }] = useBoolean(false); + const [total, setTotal] = useState(0); - const totalRef = useRef(total); - totalRef.current = total; const [data, setData] = useState([]); - const dataLengthRef = useRef(data.length); - dataLengthRef.current = data.length; + const maxPage = useMemo(() => Math.ceil(total / pageSize) || 1, [pageSize, total]); - const { mutate, isLoading } = useMutation({ - mutationFn: async (num: number = pageNum) => { - try { - const res: PagingData = await api({ - pageNum: num, - pageSize, - ...params - }); - setPageNum(num); - res.total !== undefined && setTotal(res.total); - if (type === 'scroll') { - setData((prevData) => [...prevData, ...res.data]); - } else { - setData(res.data); - } - onChange && onChange(num); - } catch (error: any) { - toast({ - title: getErrText(error, t('common:core.chat.error.data_error')), - status: 'error' - }); - console.log(error); + const fetchData = useLockFn(async (num: number = pageNum) => { + if (noMore.current && num !== 1) return; + setTrue(); + + try { + const res: PagingData = await api({ + pageNum: num, + pageSize, + ...params + }); + + // Check total and set + res.total !== undefined && setTotal(res.total); + + if (res.total !== undefined && res.total <= data.length + res.data.length) { + noMore.current = true; + } + + setPageNum(num); + + if (type === 'scroll') { + setData((prevData) => (num === 1 ? res.data : [...prevData, ...res.data])); + } else { + setData(res.data); } - return null; + + onChange?.(num); + } catch (error: any) { + toast({ + title: getErrText(error, t('common:core.chat.error.data_error')), + status: 'error' + }); + console.log(error); } + + setFalse(); }); const Pagination = useCallback(() => { @@ -82,7 +99,7 @@ export function usePagination({ aria-label={'left'} size={'smSquare'} isLoading={isLoading} - onClick={() => mutate(pageNum - 1)} + onClick={() => fetchData(pageNum - 1)} /> ({ const val = +e.target.value; if (val === pageNum) return; if (val >= maxPage) { - mutate(maxPage); + fetchData(maxPage); } else if (val < 1) { - mutate(1); + fetchData(1); } else { - mutate(+e.target.value); + fetchData(+e.target.value); } }} onKeyDown={(e) => { @@ -110,11 +127,11 @@ export function usePagination({ if (val && e.key === 'Enter') { if (val === pageNum) return; if (val >= maxPage) { - mutate(maxPage); + fetchData(maxPage); } else if (val < 1) { - mutate(1); + fetchData(1); } else { - mutate(val); + fetchData(val); } } }} @@ -130,22 +147,35 @@ export function usePagination({ isLoading={isLoading} w={'28px'} h={'28px'} - onClick={() => mutate(pageNum + 1)} + onClick={() => fetchData(pageNum + 1)} /> ); - }, [isLoading, maxPage, mutate, pageNum]); + }, [isLoading, maxPage, fetchData, pageNum]); + + // Reload data + const { runAsync: refresh } = useRequest( + async () => { + setData([]); + defaultRequest && fetchData(1); + }, + { + manual: false, + refreshDeps, + throttleWait: 100 + } + ); - const ScrollData = useCallback( - ({ children, ...props }: { children: React.ReactNode }) => { - const loadText = useMemo(() => { + const ScrollData = useMemoizedFn( + ({ children, ...props }: { children: React.ReactNode } & BoxProps) => { + const loadText = (() => { if (isLoading) return t('common:common.is_requesting'); if (total <= data.length) return t('common:common.request_end'); return t('common:common.request_more'); - }, []); + })(); return ( - + {children} ({ cursor={loadText === t('common:common.request_more') ? 'pointer' : 'default'} onClick={() => { if (loadText !== t('common:common.request_more')) return; - mutate(pageNum + 1); + fetchData(pageNum + 1); }} > {loadText} ); - }, - [data.length, isLoading, mutate, pageNum, total] + } ); - useEffect(() => { - if (!elementRef?.current || type !== 'scroll') return; - - const scrolling = throttle((e: Event) => { - const element = e.target as HTMLDivElement; - if (!element) return; - // 当前滚动位置 - const scrollTop = element.scrollTop; - // 可视高度 - const clientHeight = element.clientHeight; - // 内容总高度 - const scrollHeight = element.scrollHeight; - // 判断是否滚动到底部 - if ( - scrollTop + clientHeight + thresholdVal >= scrollHeight && - dataLengthRef.current < totalRef.current - ) { - mutate(pageNumRef.current + 1); + // Scroll check + const scroll = useScroll(ScrollContainerRef); + useThrottleEffect( + () => { + if (!ScrollContainerRef?.current || type !== 'scroll' || total === 0) return; + const { scrollTop, scrollHeight, clientHeight } = ScrollContainerRef.current; + if (scrollTop + clientHeight >= scrollHeight - 100) { + fetchData(pageNum + 1); } - }, 100); - - const handleScroll = (e: Event) => { - scrolling(e); - }; - - elementRef.current.addEventListener('scroll', handleScroll); - return () => { - elementRef.current?.removeEventListener('scroll', handleScroll); - }; - }, [elementRef, mutate, pageNum, type, total, data.length]); - - useEffect(() => { - defaultRequest && mutate(1); - }, []); + }, + [scroll], + { wait: 50 } + ); return { pageNum, @@ -210,6 +218,7 @@ export function usePagination({ isLoading, Pagination, ScrollData, - getData: mutate + getData: fetchData, + refresh }; } diff --git a/packages/web/hooks/useScrollPagination.tsx b/packages/web/hooks/useScrollPagination.tsx index 4c8e18cd028e..d27e382e4df7 100644 --- a/packages/web/hooks/useScrollPagination.tsx +++ b/packages/web/hooks/useScrollPagination.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useState, useEffect } from 'react'; +import React, { useRef, useState } from 'react'; import { Box, BoxProps } from '@chakra-ui/react'; import { useToast } from './useToast'; import { getErrText } from '@fastgpt/global/common/error/utils'; @@ -9,11 +9,14 @@ import { useMemoizedFn, useScroll, useVirtualList, - useRequest + useRequest, + useThrottleEffect } from 'ahooks'; import MyBox from '../components/common/MyBox'; import { useTranslation } from 'next-i18next'; +type ItemHeight = (index: number, data: T) => number; + export type ScrollListType = ({ children, EmptyChildren, @@ -31,8 +34,6 @@ export function useScrollPagination< >( api: (data: TParams) => Promise, { - debounceWait, - throttleWait, refreshDeps, itemHeight = 50, overscan = 10, @@ -40,11 +41,9 @@ export function useScrollPagination< pageSize = 10, defaultParams = {} }: { - debounceWait?: number; - throttleWait?: number; refreshDeps?: any[]; - itemHeight: number; + itemHeight: number | ItemHeight; overscan?: number; pageSize?: number; @@ -123,43 +122,56 @@ export function useScrollPagination< isLoading?: boolean; } & BoxProps) => { return ( - <> - - {children} + + + {children} {noMore.current && list.length > 0 && ( {t('common:common.No more data')} )} - {list.length === 0 && !isLoading && EmptyChildren && <>{EmptyChildren}} - - + + + {list.length === 0 && !isLoading && EmptyChildren && <>{EmptyChildren}} + ); } ); - useRequest(() => loadData(1), { - refreshDeps, - debounceWait: data.length === 0 ? 0 : debounceWait, - throttleWait - }); + // Reload data + useRequest( + async () => { + console.log('reload', 11111); + loadData(1); + }, + { + manual: false, + refreshDeps + } + ); + // Check if scroll to bottom const scroll = useScroll(containerRef); - useEffect(() => { - if (!containerRef.current || list.length === 0) return; - - const { scrollTop, scrollHeight, clientHeight } = containerRef.current; - - if (scrollTop + clientHeight >= scrollHeight - 100) { - loadData(current + 1); + useThrottleEffect( + () => { + if (!containerRef.current || list.length === 0) return; + const { scrollTop, scrollHeight, clientHeight } = containerRef.current; + console.log('=======', 111111); + if (scrollTop + clientHeight >= scrollHeight - 100) { + loadData(current + 1); + } + }, + [scroll], + { + wait: 50 } - }, [scroll]); + ); return { containerRef, - list, + scrollDataList: list, total, - data, + totalData: data, setData, isLoading, ScrollList, diff --git a/projects/app/src/components/core/app/InputGuideConfig.tsx b/projects/app/src/components/core/app/InputGuideConfig.tsx index 0965f14af0e0..fd93d55001c4 100644 --- a/projects/app/src/components/core/app/InputGuideConfig.tsx +++ b/projects/app/src/components/core/app/InputGuideConfig.tsx @@ -198,7 +198,7 @@ const LexiconConfigModal = ({ appId, onClose }: { appId: string; onClose: () => }); const { - list, + scrollDataList, setData, ScrollList, isLoading: isRequesting, @@ -206,7 +206,7 @@ const LexiconConfigModal = ({ appId, onClose }: { appId: string; onClose: () => scroll2Top } = useScrollPagination(getChatInputGuideList, { refreshDeps: [searchKey], - debounceWait: 300, + // debounceWait: 300, itemHeight: 48, overscan: 20, @@ -389,7 +389,7 @@ const LexiconConfigModal = ({ appId, onClose }: { appId: string; onClose: () => {/* new data input */} {newData !== undefined && ( - 0 ? 7 : 0}> + 0 ? 7 : 0}> } @@ -412,7 +412,7 @@ const LexiconConfigModal = ({ appId, onClose }: { appId: string; onClose: () => fontSize={'sm'} EmptyChildren={} > - {list.map((data, index) => { + {scrollDataList.map((data, index) => { const item = data.data; const selected = selectedRows.includes(item._id); diff --git a/projects/app/src/pages/api/core/chat/getHistories.ts b/projects/app/src/pages/api/core/chat/getHistories.ts index 828c5d336e94..19ecbb0112f8 100644 --- a/projects/app/src/pages/api/core/chat/getHistories.ts +++ b/projects/app/src/pages/api/core/chat/getHistories.ts @@ -1,18 +1,27 @@ -import type { NextApiRequest, NextApiResponse } from 'next'; -import { jsonRes } from '@fastgpt/service/common/response'; import { connectToDatabase } from '@/service/mongo'; import { MongoChat } from '@fastgpt/service/core/chat/chatSchema'; -import type { ChatHistoryItemType } from '@fastgpt/global/core/chat/type.d'; import { ChatSourceEnum } from '@fastgpt/global/core/chat/constants'; -import { GetHistoriesProps } from '@/global/core/chat/api'; import { authOutLink } from '@/service/support/permission/auth/outLink'; import { authCert } from '@fastgpt/service/support/permission/auth/common'; import { authTeamSpaceToken } from '@/service/support/permission/auth/team'; +import { NextAPI } from '@/service/middleware/entry'; +import { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next'; +import { PaginationProps, PaginationResponse } from '@fastgpt/web/common/fetch/type'; +import { GetHistoriesProps } from '@/global/core/chat/api'; +export type getHistoriesQuery = {}; + +export type getHistoriesBody = PaginationProps; -export default async function handler(req: NextApiRequest, res: NextApiResponse) { +export type getHistoriesResponse = {}; + +async function handler( + req: ApiRequestProps, + res: ApiResponseType +): Promise> { try { await connectToDatabase(); - const { appId, shareId, outLinkUid, teamId, teamToken } = req.body as GetHistoriesProps; + const { appId, shareId, outLinkUid, teamId, teamToken, current, pageSize } = + req.body as getHistoriesBody; const limit = shareId && outLinkUid ? 20 : 30; @@ -50,24 +59,28 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) return Promise.reject('Params are error'); })(); - const data = await MongoChat.find(match, 'chatId title top customTitle appId updateTime') - .sort({ top: -1, updateTime: -1 }) - .limit(limit); + const [data, total] = await Promise.all([ + await MongoChat.find(match, 'chatId title top customTitle appId updateTime') + .sort({ top: -1, updateTime: -1 }) + .skip((current - 1) * pageSize) + .limit(pageSize), + MongoChat.countDocuments(match) + ]); - jsonRes(res, { - data: data.map((item) => ({ + return { + list: data.map((item) => ({ chatId: item.chatId, updateTime: item.updateTime, appId: item.appId, customTitle: item.customTitle, title: item.title, top: item.top - })) - }); + })), + total + }; } catch (err) { - jsonRes(res, { - code: 500, - error: err - }); + return Promise.reject(err); } } + +export default NextAPI(handler); diff --git a/projects/app/src/pages/app/detail/components/PublishHistoriesSlider.tsx b/projects/app/src/pages/app/detail/components/PublishHistoriesSlider.tsx index d9935cb19eb0..b2a14d9cba0c 100644 --- a/projects/app/src/pages/app/detail/components/PublishHistoriesSlider.tsx +++ b/projects/app/src/pages/app/detail/components/PublishHistoriesSlider.tsx @@ -41,11 +41,11 @@ const PublishHistoriesSlider = ({ const [selectedHistoryId, setSelectedHistoryId] = useState(); - const { list, ScrollList, isLoading } = useScrollPagination(getPublishList, { + const { scrollDataList, ScrollList, isLoading } = useScrollPagination(getPublishList, { itemHeight: 49, overscan: 20, - pageSize: 30, + pageSize: 20, defaultParams: { appId } @@ -132,7 +132,7 @@ const PublishHistoriesSlider = ({ {appT('current_settings')} - {list.map((data, index) => { + {scrollDataList.map((data, index) => { const item = data.data; return ( @@ -159,7 +159,7 @@ const PublishHistoriesSlider = ({ borderColor={'primary.600'} borderRadius={'50%'} position={'relative'} - {...(index !== list.length - 1 && { + {...(index !== scrollDataList.length - 1 && { _after: { content: '""', height: '40px', diff --git a/projects/app/src/pages/app/detail/components/WorkflowPublishHistoriesSlider.tsx b/projects/app/src/pages/app/detail/components/WorkflowPublishHistoriesSlider.tsx index 95b09c4ace5a..5c0c8c425a57 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowPublishHistoriesSlider.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowPublishHistoriesSlider.tsx @@ -180,15 +180,18 @@ const TeamCloud = () => { const { loadAndGetTeamMembers } = useUserStore(); const { feConfigs } = useSystemStore(); - const { list, ScrollList, isLoading, fetchData } = useScrollPagination(getWorkflowVersionList, { - itemHeight: 40, - overscan: 20, + const { scrollDataList, ScrollList, isLoading, fetchData } = useScrollPagination( + getWorkflowVersionList, + { + itemHeight: 40, + overscan: 20, - pageSize: 30, - defaultParams: { - appId: appDetail._id + pageSize: 30, + defaultParams: { + appId: appDetail._id + } } - }); + ); const { data: members = [] } = useRequest2(loadAndGetTeamMembers, { manual: !feConfigs.isPlus }); @@ -228,9 +231,9 @@ const TeamCloud = () => { return ( - {list.map((data, index) => { + {scrollDataList.map((data, index) => { const item = data.data; - const firstPublishedIndex = list.findIndex((data) => data.data.isPublish); + const firstPublishedIndex = scrollDataList.findIndex((data) => data.data.isPublish); const tmb = members.find((member) => member.tmbId === item.tmbId); return ( diff --git a/projects/app/src/pages/chat/components/ChatHistorySlider.tsx b/projects/app/src/pages/chat/components/ChatHistorySlider.tsx index 28045849b779..6624497c0802 100644 --- a/projects/app/src/pages/chat/components/ChatHistorySlider.tsx +++ b/projects/app/src/pages/chat/components/ChatHistorySlider.tsx @@ -9,8 +9,6 @@ import MyIcon from '@fastgpt/web/components/common/Icon'; import { useTranslation } from 'next-i18next'; import { useConfirm } from '@fastgpt/web/hooks/useConfirm'; import { useUserStore } from '@/web/support/user/useUserStore'; -import { AppListItemType } from '@fastgpt/global/core/app/type'; -import { useI18n } from '@/web/context/I18n'; import MyMenu from '@fastgpt/web/components/common/MyMenu'; import { useContextSelector } from 'use-context-selector'; import { ChatContext } from '@/web/core/chat/context/chatContext'; @@ -52,19 +50,19 @@ const ChatHistorySlider = ({ const { userInfo } = useUserStore(); const { - histories, onChangeChatId, chatId: activeChatId, - isLoading + isLoading, + ScrollList, + historyList, + histories } = useContextSelector(ChatContext, (v) => v); const concatHistory = useMemo(() => { - const formatHistories: HistoryItemType[] = histories.map((item) => ({ - id: item.chatId, - title: item.title, - customTitle: item.customTitle, - top: item.top - })); + const formatHistories: HistoryItemType[] = historyList.map((data) => { + const item = data.data; + return { id: item.chatId, title: item.title, customTitle: item.customTitle, top: item.top }; + }); const newChat: HistoryItemType = { id: activeChatId, title: t('common:core.chat.New Chat') @@ -72,7 +70,7 @@ const ChatHistorySlider = ({ const activeChat = histories.find((item) => item.chatId === activeChatId); return !activeChat ? [newChat].concat(formatHistories) : formatHistories; - }, [activeChatId, histories, t]); + }, [activeChatId, histories, historyList, t]); // custom title edit const { onOpenModal, EditModal: EditTitleModal } = useEditTitle({ @@ -175,20 +173,19 @@ const ChatHistorySlider = ({ )} - + {/* chat history */} <> {concatHistory.map((item, i) => ( ))} - + {/* exec */} {!isPc && isUserChatPage && ( diff --git a/projects/app/src/pages/chat/index.tsx b/projects/app/src/pages/chat/index.tsx index bf51a6c2d2d6..8cc9e4597d2a 100644 --- a/projects/app/src/pages/chat/index.tsx +++ b/projects/app/src/pages/chat/index.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import NextHead from '@/components/common/NextHead'; import { useRouter } from 'next/router'; import { delChatRecordById, getChatHistories, getInitChatInfo } from '@/web/core/chat/api'; @@ -53,14 +53,17 @@ const Chat = ({ const { setLastChatAppId } = useChatStore(); const { - loadHistories, + setHistories: setRecordHistories, + loadHistories: loadRecordHistories, + histories: recordHistories, onUpdateHistory, onClearHistories, onDelHistory, isOpenSlider, onCloseSlider, forbidLoadChat, - onChangeChatId + onChangeChatId, + onUpdateHistoryTitle } = useContextSelector(ChatContext, (v) => v); const { ChatBoxRef, @@ -148,8 +151,7 @@ const Chat = ({ if (completionChatId !== chatId && controller.signal.reason !== 'leave') { onChangeChatId(completionChatId, true); } - loadHistories(); - + onUpdateHistoryTitle({ chatId: completionChatId, newTitle }); // update chat window setChatData((state) => ({ ...state, @@ -158,7 +160,7 @@ const Chat = ({ return { responseText, responseData, isNewChat: forbidLoadChat.current }; }, - [appId, chatId, forbidLoadChat, loadHistories, onChangeChatId] + [chatId, appId, onUpdateHistoryTitle, forbidLoadChat, onChangeChatId] ); return ( @@ -283,14 +285,6 @@ const Render = (props: Props) => { } ); - const { data: histories = [], runAsync: loadHistories } = useRequest2( - () => (appId ? getChatHistories({ appId }) : Promise.resolve([])), - { - manual: false, - refreshDeps: [appId] - } - ); - // 初始化聊天框 useMount(async () => { // pc: redirect to latest model chat @@ -324,8 +318,9 @@ const Render = (props: Props) => { } }); + const providerParams = useMemo(() => ({ appId }), [appId]); return ( - + ); diff --git a/projects/app/src/pages/chat/share.tsx b/projects/app/src/pages/chat/share.tsx index 43ff1bf2c3b5..3daccd21e8d6 100644 --- a/projects/app/src/pages/chat/share.tsx +++ b/projects/app/src/pages/chat/share.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useRef, useState } from 'react'; +import React, { useCallback, useMemo, useRef, useState } from 'react'; import { useRouter } from 'next/router'; import { Box, Flex, Drawer, DrawerOverlay, DrawerContent } from '@chakra-ui/react'; import { streamFetch } from '@/web/common/api/fetch'; @@ -75,6 +75,7 @@ const OutLink = ({ appName, appIntro, appAvatar }: Props) => { const outLinkUid: string = authToken || localUId; const { + onUpdateHistoryTitle, loadHistories, onUpdateHistory, onClearHistories, @@ -140,7 +141,7 @@ const OutLink = ({ appName, appIntro, appAvatar }: Props) => { if (completionChatId !== chatId) { onChangeChatId(completionChatId, true); } - loadHistories(); + onUpdateHistoryTitle({ chatId: completionChatId, newTitle }); // update chat window setChatData((state) => ({ @@ -168,9 +169,9 @@ const OutLink = ({ appName, appIntro, appAvatar }: Props) => { shareId, chatData.app.type, outLinkUid, + onUpdateHistoryTitle, forbidLoadChat, - onChangeChatId, - loadHistories + onChangeChatId ] ); @@ -354,16 +355,12 @@ const Render = (props: Props) => { const { localUId } = useShareChatStore(); const outLinkUid: string = authToken || localUId; - const { data: histories = [], runAsync: loadHistories } = useRequest2( - () => (shareId && outLinkUid ? getChatHistories({ shareId, outLinkUid }) : Promise.resolve([])), - { - manual: false, - refreshDeps: [shareId, outLinkUid] - } - ); + const contextParams = useMemo(() => { + return { shareId, outLinkUid }; + }, [shareId, outLinkUid]); return ( - + ; ); diff --git a/projects/app/src/pages/chat/team.tsx b/projects/app/src/pages/chat/team.tsx index 9d613e875264..6b96c2a2e358 100644 --- a/projects/app/src/pages/chat/team.tsx +++ b/projects/app/src/pages/chat/team.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import NextHead from '@/components/common/NextHead'; import { delChatRecordById, getChatHistories, getTeamChatInfo } from '@/web/core/chat/api'; import { useRouter } from 'next/router'; @@ -58,6 +58,7 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => { const [chatData, setChatData] = useState(defaultChatData); const { + onUpdateHistoryTitle, loadHistories, onUpdateHistory, onClearHistories, @@ -114,7 +115,7 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => { if (completionChatId !== chatId) { onChangeChatId(completionChatId, true); } - loadHistories(); + onUpdateHistoryTitle({ chatId: completionChatId, newTitle }); // update chat window setChatData((state) => ({ @@ -125,15 +126,15 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => { return { responseText, responseData, isNewChat: forbidLoadChat.current }; }, [ - chatData.app.type, chatId, customVariables, appId, teamId, teamToken, + chatData.app.type, + onUpdateHistoryTitle, forbidLoadChat, - onChangeChatId, - loadHistories + onChangeChatId ] ); @@ -302,19 +303,6 @@ const Render = (props: Props) => { } ); - const { data: histories = [], runAsync: loadHistories } = useRequest2( - async () => { - if (teamId && appId && teamToken) { - return getChatHistories({ teamId, appId, teamToken: teamToken }); - } - return []; - }, - { - manual: false, - refreshDeps: [appId, teamId, teamToken] - } - ); - // 初始化聊天框 useEffect(() => { (async () => { @@ -330,8 +318,12 @@ const Render = (props: Props) => { })(); }, [appId, loadMyApps, myApps, router, t, toast]); + const contextParams = useMemo(() => { + return { teamId, appId, teamToken }; + }, [teamId, appId, teamToken]); + return ( - + ); diff --git a/projects/app/src/pages/dataset/detail/components/CollectionCard/Context.tsx b/projects/app/src/pages/dataset/detail/components/CollectionCard/Context.tsx index e9de232ecc0e..482da3b52f3f 100644 --- a/projects/app/src/pages/dataset/detail/components/CollectionCard/Context.tsx +++ b/projects/app/src/pages/dataset/detail/components/CollectionCard/Context.tsx @@ -120,11 +120,9 @@ const CollectionPageContextProvider = ({ children }: { children: ReactNode }) => searchText, filterTags }, - defaultRequest: false + // defaultRequest: false, + refreshDeps: [parentId, searchText, filterTags] }); - useEffect(() => { - getData(1); - }, [parentId]); const contextValue: CollectionPageContextType = { openWebSyncConfirm: openWebSyncConfirm(onUpdateDatasetWebsiteConfig), diff --git a/projects/app/src/pages/dataset/detail/components/CollectionCard/Header.tsx b/projects/app/src/pages/dataset/detail/components/CollectionCard/Header.tsx index 30b9ac82d62d..3bf967ddd3ac 100644 --- a/projects/app/src/pages/dataset/detail/components/CollectionCard/Header.tsx +++ b/projects/app/src/pages/dataset/detail/components/CollectionCard/Header.tsx @@ -60,15 +60,6 @@ const Header = ({}: {}) => { const { searchText, setSearchText, total, getData, pageNum, onOpenWebsiteModal } = useContextSelector(CollectionPageContext, (v) => v); - // change search - const debounceRefetch = useCallback( - debounce(() => { - getData(1); - lastSearch.current = searchText; - }, 300), - [] - ); - const { data: paths = [] } = useQuery(['getDatasetCollectionPathById', parentId], () => getDatasetCollectionPathById(parentId) ); @@ -189,17 +180,6 @@ const Header = ({}: {}) => { } onChange={(e) => { setSearchText(e.target.value); - debounceRefetch(); - }} - onBlur={() => { - if (searchText === lastSearch.current) return; - getData(1); - }} - onKeyDown={(e) => { - if (searchText === lastSearch.current) return; - if (e.key === 'Enter') { - getData(1); - } }} /> )} diff --git a/projects/app/src/pages/dataset/detail/components/CollectionCard/HeaderTagPopOver.tsx b/projects/app/src/pages/dataset/detail/components/CollectionCard/HeaderTagPopOver.tsx index c13d195ebb3c..4eadfb4fe54b 100644 --- a/projects/app/src/pages/dataset/detail/components/CollectionCard/HeaderTagPopOver.tsx +++ b/projects/app/src/pages/dataset/detail/components/CollectionCard/HeaderTagPopOver.tsx @@ -5,15 +5,13 @@ import MyBox from '@fastgpt/web/components/common/MyBox'; import { useContextSelector } from 'use-context-selector'; import { DatasetPageContext } from '@/web/core/dataset/context/datasetPageContext'; import { useTranslation } from 'next-i18next'; -import { useCallback, useState } from 'react'; import { CollectionPageContext } from './Context'; -import { debounce, isEqual } from 'lodash'; +import { isEqual } from 'lodash'; import TagManageModal from './TagManageModal'; import { DatasetTagType } from '@fastgpt/global/core/dataset/type'; const HeaderTagPopOver = () => { const { t } = useTranslation(); - const [checkedTags, setCheckedTags] = useState([]); const { searchDatasetTagsResult, @@ -29,12 +27,8 @@ const HeaderTagPopOver = () => { CollectionPageContext, (v) => v ); - const debounceRefetch = useCallback( - debounce(() => { - getData(1); - }, 300), - [] - ); + + const checkedTags = filterTags; const { isOpen: isTagManageModalOpen, @@ -46,16 +40,13 @@ const HeaderTagPopOver = () => { let currentCheckedTags = []; if (checkedTags.includes(tag._id)) { currentCheckedTags = checkedTags.filter((t) => t !== tag._id); - setCheckedTags(currentCheckedTags); setCheckedDatasetTag(checkedDatasetTag.filter((t) => t._id !== tag._id)); } else { currentCheckedTags = [...checkedTags, tag._id]; - setCheckedTags([...checkedTags, tag._id]); setCheckedDatasetTag([...checkedDatasetTag, tag]); } if (isEqual(currentCheckedTags, filterTags)) return; setFilterTags(currentCheckedTags); - debounceRefetch(); }; return ( @@ -181,9 +172,7 @@ const HeaderTagPopOver = () => { variant={'unstyled'} onClick={() => { setSearchTagKey(''); - setCheckedTags([]); setFilterTags([]); - debounceRefetch(); onClose(); }} > @@ -211,7 +200,7 @@ const HeaderTagPopOver = () => { { onCloseTagManageModal(); - debounceRefetch(); + getData(1); }} /> )} diff --git a/projects/app/src/pages/dataset/detail/components/CollectionCard/TagManageModal.tsx b/projects/app/src/pages/dataset/detail/components/CollectionCard/TagManageModal.tsx index b5edff03c01a..edf1b5679ed0 100644 --- a/projects/app/src/pages/dataset/detail/components/CollectionCard/TagManageModal.tsx +++ b/projects/app/src/pages/dataset/detail/components/CollectionCard/TagManageModal.tsx @@ -121,14 +121,15 @@ const TagManageModal = ({ onClose }: { onClose: () => void }) => { // Tags list const { - list, + scrollDataList: renderTags, + totalData: collectionTags, ScrollList, isLoading: isRequesting, fetchData, total: tagsTotal } = useScrollPagination(getDatasetCollectionTags, { refreshDeps: [''], - debounceWait: 300, + // debounceWait: 300, itemHeight: 56, overscan: 10, @@ -142,12 +143,12 @@ const TagManageModal = ({ onClose }: { onClose: () => void }) => { // Collections list const { - list: collectionsList, + scrollDataList: collectionsList, ScrollList: ScrollListCollections, isLoading: collectionsListLoading } = useScrollPagination(getScrollCollectionList, { refreshDeps: [searchText], - debounceWait: 300, + // debounceWait: 300, itemHeight: 37, overscan: 10, @@ -221,7 +222,7 @@ const TagManageModal = ({ onClose }: { onClose: () => void }) => { ref={tagInputRef} w={'200px'} onBlur={() => { - if (newTag && !list.map((item) => item.data.tag).includes(newTag)) { + if (newTag && !collectionTags.map((item) => item.tag).includes(newTag)) { onCreateCollectionTag(newTag); } setNewTag(undefined); @@ -236,7 +237,7 @@ const TagManageModal = ({ onClose }: { onClose: () => void }) => { fontSize={'sm'} EmptyChildren={} > - {list.map((listItem) => { + {renderTags.map((listItem) => { const item = listItem.data; const tagUsage = tagUsages?.find((tagUsage) => tagUsage.tagId === item._id); const collections = tagUsage?.collections || []; @@ -292,7 +293,9 @@ const TagManageModal = ({ onClose }: { onClose: () => void }) => { onBlur={() => { if ( currentEditTagContent && - !list.map((item) => item.data.tag).includes(currentEditTagContent) + !collectionTags + .map((item) => item.tag) + .includes(currentEditTagContent) ) { onUpdateCollectionTag({ tag: currentEditTagContent, diff --git a/projects/app/src/pages/dataset/detail/components/DataCard.tsx b/projects/app/src/pages/dataset/detail/components/DataCard.tsx index 2ae8b97e97f3..1bb5bec53986 100644 --- a/projects/app/src/pages/dataset/detail/components/DataCard.tsx +++ b/projects/app/src/pages/dataset/detail/components/DataCard.tsx @@ -29,11 +29,10 @@ import TagsPopOver from './CollectionCard/TagsPopOver'; import { useSystemStore } from '@/web/common/system/useSystemStore'; import MyDivider from '@fastgpt/web/components/common/MyDivider'; import Markdown from '@/components/Markdown'; +import { DatasetDataListItemType } from '@/global/core/dataset/type'; const DataCard = () => { - const BoxRef = useRef(null); const theme = useTheme(); - const lastSearch = useRef(''); const router = useRouter(); const { isPc } = useSystem(); const { collectionId = '', datasetId } = router.query as { @@ -51,44 +50,30 @@ const DataCard = () => { type: 'delete' }); + const scrollParams = useMemo( + () => ({ + collectionId, + searchText + }), + [collectionId, searchText] + ); const { data: datasetDataList, - Pagination, + ScrollData, total, - getData, - pageNum, - pageSize, - isLoading: isRequesting - } = usePagination({ + isLoading, + refresh, + setData: setDatasetDataList + } = usePagination({ api: getDatasetDataList, - pageSize: 24, - defaultRequest: false, - params: { - collectionId, - searchText - }, - onChange() { - if (BoxRef.current) { - BoxRef.current.scrollTop = 0; - } - } + pageSize: 10, + type: 'scroll', + params: scrollParams, + refreshDeps: [searchText, collectionId] }); const [editDataId, setEditDataId] = useState(); - // get first page data - useRequest2( - async () => { - getData(1); - lastSearch.current = searchText; - }, - { - manual: false, - debounceWait: 300, - refreshDeps: [searchText] - } - ); - // get file info const { data: collection } = useQuery( ['getDatasetCollectionById', collectionId], @@ -106,17 +91,9 @@ const DataCard = () => { const canWrite = useMemo(() => datasetDetail.permission.hasWritePer, [datasetDetail]); - const { loading } = useRequest2(putDatasetDataById, { - onSuccess() { - getData(pageNum); - } - }); - - const isLoading = isRequesting || loading; - return ( - - + + {/* Header */} @@ -185,7 +162,7 @@ const DataCard = () => { /> {/* data */} - + {datasetDataList.map((item, index) => ( { boxShadow: 'lg', '& .header': { visibility: 'visible' }, '& .footer': { visibility: 'visible' }, - '& .forbid-switch': { display: 'flex' }, bg: index % 2 === 1 ? 'myGray.200' : 'blue.100' }} onClick={(e) => { @@ -298,13 +274,18 @@ const DataCard = () => { icon={} variant={'whiteDanger'} size={'xsSquare'} - aria-label={'delete'} onClick={(e) => { e.stopPropagation(); openConfirm(async () => { try { await delOneDatasetDataById(item._id); - getData(pageNum); + setDatasetDataList((prev) => { + return prev.filter((data) => data._id !== item._id); + }); + toast({ + title: t('common:common.Delete Success'), + status: 'success' + }); } catch (error) { toast({ title: getErrText(error), @@ -313,19 +294,17 @@ const DataCard = () => { } })(); }} + aria-label={''} /> )} ))} - {total > pageSize && ( - - - - )} - {total === 0 && } - + + {total === 0 && !isLoading && ( + + )} {editDataId !== undefined && collection && ( @@ -333,7 +312,23 @@ const DataCard = () => { collectionId={collection._id} dataId={editDataId} onClose={() => setEditDataId(undefined)} - onSuccess={() => getData(pageNum)} + onSuccess={(data) => { + if (editDataId === '') { + refresh(); + return; + } + setDatasetDataList((prev) => { + return prev.map((item) => { + if (item._id === editDataId) { + return { + ...item, + ...data + }; + } + return item; + }); + }); + }} /> )} diff --git a/projects/app/src/web/core/chat/api.ts b/projects/app/src/web/core/chat/api.ts index 953e29097767..ba11afb638a6 100644 --- a/projects/app/src/web/core/chat/api.ts +++ b/projects/app/src/web/core/chat/api.ts @@ -20,6 +20,7 @@ import type { import { UpdateChatFeedbackProps } from '@fastgpt/global/core/chat/api'; import { AuthTeamTagTokenProps } from '@fastgpt/global/support/user/team/tag'; import { AppListItemType } from '@fastgpt/global/core/app/type'; +import { PaginationProps, PaginationResponse } from '@fastgpt/web/common/fetch/type'; /** * 获取初始化聊天内容 @@ -33,8 +34,8 @@ export const getTeamChatInfo = (data: InitTeamChatProps) => /** * get current window history(appid or shareId) */ -export const getChatHistories = (data: GetHistoriesProps) => - POST('/core/chat/getHistories', data); +export const getChatHistories = (data: PaginationProps) => + POST>('/core/chat/getHistories', data); /** * get detail responseData by dataId appId chatId */ diff --git a/projects/app/src/web/core/chat/context/chatContext.tsx b/projects/app/src/web/core/chat/context/chatContext.tsx index dd0335dbb88a..71adcc753732 100644 --- a/projects/app/src/web/core/chat/context/chatContext.tsx +++ b/projects/app/src/web/core/chat/context/chatContext.tsx @@ -2,18 +2,23 @@ import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { useRouter } from 'next/router'; import React, { ReactNode, useCallback, useEffect, useRef } from 'react'; import { createContext } from 'use-context-selector'; -import { delClearChatHistories, delChatHistoryById, putChatHistory } from '../api'; +import { + delClearChatHistories, + delChatHistoryById, + putChatHistory, + getChatHistories +} from '../api'; import { ChatHistoryItemType } from '@fastgpt/global/core/chat/type'; import { ClearHistoriesProps, DelHistoryProps, UpdateHistoryProps } from '@/global/core/chat/api'; -import { useDisclosure } from '@chakra-ui/react'; +import { BoxProps, useDisclosure } from '@chakra-ui/react'; import { useChatStore } from './storeChat'; import { getNanoid } from '@fastgpt/global/common/string/tools'; +import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination'; type ChatContextValueType = { - histories: ChatHistoryItemType[]; - loadHistories: () => Promise; + params: Record; }; -type ChatContextType = ChatContextValueType & { +type ChatContextType = { chatId: string; onUpdateHistory: (data: UpdateHistoryProps) => void; onDelHistory: (data: DelHistoryProps) => Promise; @@ -21,17 +26,45 @@ type ChatContextType = ChatContextValueType & { isOpenSlider: boolean; onCloseSlider: () => void; onOpenSlider: () => void; + setHistories: React.Dispatch>; forbidLoadChat: React.MutableRefObject; onChangeChatId: (chatId?: string, forbid?: boolean) => void; + loadHistories: () => void; + ScrollList: ({ + children, + EmptyChildren, + isLoading, + ...props + }: { + children: React.ReactNode; + EmptyChildren?: React.ReactNode; + isLoading?: boolean; + } & BoxProps) => ReactNode; onChangeAppId: (appId: string) => void; isLoading: boolean; + historyList: { + index: number; + data: ChatHistoryItemType; + }[]; + histories: ChatHistoryItemType[]; + onUpdateHistoryTitle: ({ chatId, newTitle }: { chatId: string; newTitle: string }) => void; }; export const ChatContext = createContext({ chatId: '', // forbidLoadChat: undefined, + historyList: [], histories: [], - loadHistories: function (): Promise { + onUpdateHistoryTitle: function (): void { + throw new Error('Function not implemented.'); + }, + ScrollList: function (): ReactNode { + throw new Error('Function not implemented.'); + }, + loadHistories: function (): void { + throw new Error('Function not implemented.'); + }, + setHistories: function (): void { throw new Error('Function not implemented.'); }, onUpdateHistory: function (data: UpdateHistoryProps): void { @@ -62,8 +95,7 @@ export const ChatContext = createContext({ const ChatContextProvider = ({ children, - histories, - loadHistories + params }: ChatContextValueType & { children: ReactNode }) => { const router = useRouter(); const { chatId = '' } = router.query as { chatId: string }; @@ -72,6 +104,21 @@ const ChatContextProvider = ({ const { isOpen: isOpenSlider, onClose: onCloseSlider, onOpen: onOpenSlider } = useDisclosure(); + const { + scrollDataList: historyList, + ScrollList, + isLoading: isPaginationLoading, + setData: setHistories, + fetchData: loadHistories, + totalData: histories + } = useScrollPagination(getChatHistories, { + overscan: 30, + pageSize: 30, + itemHeight: 52, + defaultParams: params, + refreshDeps: [params] + }); + const { setLastChatId } = useChatStore(); const onChangeChatId = useCallback( (changeChatId = getNanoid(), forbid = false) => { @@ -85,10 +132,13 @@ const ChatContextProvider = ({ } }); } + onCloseSlider(); }, [chatId, onCloseSlider, router, setLastChatId] ); + + // Refresh lastChatId useEffect(() => { setLastChatId(chatId); }, [chatId, setLastChatId]); @@ -108,33 +158,68 @@ const ChatContextProvider = ({ ); const { runAsync: onUpdateHistory, loading: isUpdatingHistory } = useRequest2(putChatHistory, { - onSuccess() { - loadHistories(); + onSuccess(data, params) { + const { chatId, top, customTitle } = params[0]; + + setHistories((histories) => { + const updatedHistories = histories.map((history) => { + if (history.chatId === chatId) { + return { + ...history, + customTitle: customTitle || history.customTitle, + top: top !== undefined ? top : history.top + }; + } + return history; + }); + + return top !== undefined + ? updatedHistories.sort((a, b) => (b.top ? 1 : 0) - (a.top ? 1 : 0)) + : updatedHistories; + }); }, errorToast: undefined }); + const { runAsync: onDelHistory, loading: isDeletingHistory } = useRequest2(delChatHistoryById, { - onSuccess() { - loadHistories(); + onSuccess(data, params) { + const { chatId } = params[0]; + setHistories((old) => old.filter((i) => i.chatId !== chatId)); } }); + const { runAsync: onClearHistories, loading: isClearingHistory } = useRequest2( delClearChatHistories, { onSuccess() { - loadHistories(); + setHistories([]); }, onFinally() { onChangeChatId(''); } } ); - const isLoading = isUpdatingHistory || isDeletingHistory || isClearingHistory; + + const onUpdateHistoryTitle = useCallback( + ({ chatId, newTitle }: { chatId: string; newTitle: string }) => { + // Chat history exists + if (histories.find((item) => item.chatId === chatId)) { + setHistories((state) => + state.map((item) => (item.chatId === chatId ? { ...item, title: newTitle } : item)) + ); + } else { + // Chat history not exists + loadHistories(); + } + }, + [histories, loadHistories, setHistories] + ); + + const isLoading = + isUpdatingHistory || isDeletingHistory || isClearingHistory || isPaginationLoading; const contextValue = { chatId, - histories, - loadHistories, onUpdateHistory, onDelHistory, onClearHistories, @@ -144,7 +229,13 @@ const ChatContextProvider = ({ forbidLoadChat, onChangeChatId, onChangeAppId, - isLoading + isLoading, + historyList, + setHistories, + ScrollList, + loadHistories, + histories, + onUpdateHistoryTitle }; return {children}; };