diff --git a/web/app/components/base/chat/chat-with-history/chat-wrapper.tsx b/web/app/components/base/chat/chat-with-history/chat-wrapper.tsx
index 724ef78e758966..ad9516621074fd 100644
--- a/web/app/components/base/chat/chat-with-history/chat-wrapper.tsx
+++ b/web/app/components/base/chat/chat-with-history/chat-wrapper.tsx
@@ -34,6 +34,8 @@ const ChatWrapper = () => {
currentChatInstanceRef,
appData,
themeBuilder,
+ setChatListSize,
+ hasMore,
} = useChatWithHistoryContext()
const appConfig = useMemo(() => {
const config = appParams || {}
@@ -69,7 +71,7 @@ const ChatWrapper = () => {
useEffect(() => {
if (currentChatInstanceRef.current)
currentChatInstanceRef.current.handleStop = handleStop
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const doSend: OnSend = useCallback((message, files, last_answer) => {
@@ -163,6 +165,11 @@ const ChatWrapper = () => {
/>
: null
+ const onScrollToTop = useCallback(() => {
+ if (hasMore)
+ setChatListSize(size => size + 1)
+ }, [setChatListSize, hasMore])
+
return (
{
answerIcon={answerIcon}
hideProcessDetail
themeBuilder={themeBuilder}
+ onScrollToTop={onScrollToTop}
+ hasMoreMessages={hasMore}
/>
)
diff --git a/web/app/components/base/chat/chat-with-history/context.tsx b/web/app/components/base/chat/chat-with-history/context.tsx
index 060c178993fafc..04bdb57eaa11cb 100644
--- a/web/app/components/base/chat/chat-with-history/context.tsx
+++ b/web/app/components/base/chat/chat-with-history/context.tsx
@@ -49,6 +49,8 @@ export type ChatWithHistoryContextValue = {
handleFeedback: (messageId: string, feedback: Feedback) => void
currentChatInstanceRef: RefObject<{ handleStop: () => void }>
themeBuilder?: ThemeBuilder
+ hasMore: boolean
+ setChatListSize: (size: number | ((_size: number) => number)) => void
}
export const ChatWithHistoryContext = createContext({
@@ -59,21 +61,23 @@ export const ChatWithHistoryContext = createContext
showConfigPanelBeforeChat: false,
newConversationInputs: {},
newConversationInputsRef: { current: {} },
- handleNewConversationInputsChange: () => {},
+ handleNewConversationInputsChange: () => { },
inputsForms: [],
- handleNewConversation: () => {},
- handleStartChat: () => {},
- handleChangeConversation: () => {},
- handlePinConversation: () => {},
- handleUnpinConversation: () => {},
- handleDeleteConversation: () => {},
+ handleNewConversation: () => { },
+ handleStartChat: () => { },
+ handleChangeConversation: () => { },
+ handlePinConversation: () => { },
+ handleUnpinConversation: () => { },
+ handleDeleteConversation: () => { },
conversationRenaming: false,
- handleRenameConversation: () => {},
- handleNewConversationCompleted: () => {},
+ handleRenameConversation: () => { },
+ handleNewConversationCompleted: () => { },
chatShouldReloadKey: '',
isMobile: false,
isInstalledApp: false,
- handleFeedback: () => {},
- currentChatInstanceRef: { current: { handleStop: () => {} } },
+ handleFeedback: () => { },
+ currentChatInstanceRef: { current: { handleStop: () => { } } },
+ hasMore: false,
+ setChatListSize: (_: number | ((_size: number) => number)) => { },
})
export const useChatWithHistoryContext = () => useContext(ChatWithHistoryContext)
diff --git a/web/app/components/base/chat/chat-with-history/hooks.tsx b/web/app/components/base/chat/chat-with-history/hooks.tsx
index a67cc3cd885387..b3ee7c9b3679e4 100644
--- a/web/app/components/base/chat/chat-with-history/hooks.tsx
+++ b/web/app/components/base/chat/chat-with-history/hooks.tsx
@@ -7,6 +7,7 @@ import {
} from 'react'
import { useTranslation } from 'react-i18next'
import useSWR from 'swr'
+import useSWRInfinite from 'swr/infinite'
import { useLocalStorageState } from 'ahooks'
import produce from 'immer'
import type {
@@ -16,6 +17,7 @@ import type {
} from '../types'
import { CONVERSATION_ID_INFO } from '../constants'
import { getPrevChatList } from '../utils'
+import type { IChatItem } from '../chat/type'
import {
delConversation,
fetchAppInfo,
@@ -40,6 +42,8 @@ import { useAppFavicon } from '@/hooks/use-app-favicon'
import { InputVarType } from '@/app/components/workflow/types'
import { TransferMethod } from '@/types/app'
+type IChatWithHistory = { data: Array; limit: number; has_more: boolean }
+
export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
const isInstalledApp = useMemo(() => !!installedAppInfo, [installedAppInfo])
const { data: appInfo, isLoading: appInfoLoading, error: appInfoError } = useSWR(installedAppInfo ? null : 'appInfo', fetchAppInfo)
@@ -107,15 +111,50 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
const { data: appMeta } = useSWR(['appMeta', isInstalledApp, appId], () => fetchAppMeta(isInstalledApp, appId))
const { data: appPinnedConversationData, mutate: mutateAppPinnedConversationData } = useSWR(['appConversationData', isInstalledApp, appId, true], () => fetchConversations(isInstalledApp, appId, undefined, true, 100))
const { data: appConversationData, isLoading: appConversationDataLoading, mutate: mutateAppConversationData } = useSWR(['appConversationData', isInstalledApp, appId, false], () => fetchConversations(isInstalledApp, appId, undefined, false, 100))
- const { data: appChatListData, isLoading: appChatListDataLoading } = useSWR(chatShouldReloadKey ? ['appChatList', chatShouldReloadKey, isInstalledApp, appId] : null, () => fetchChatList(chatShouldReloadKey, isInstalledApp, appId))
+ const {
+ data: appChatListData,
+ isLoading: appChatListDataLoading,
+ setSize: setChatListSize,
+ mutate: mutateAppChatListData,
+ } = useSWRInfinite(
+ (pageIndex: number, previousPageData) => {
+ if (pageIndex === 0) {
+ return [fetchChatList, {
+ conversationId: chatShouldReloadKey,
+ installedAppId: appId,
+ isInstalledApp,
+ }]
+ }
+
+ if (previousPageData && previousPageData.has_more) {
+ return [fetchChatList, {
+ conversationId: chatShouldReloadKey,
+ installedAppId: appId,
+ isInstalledApp,
+ firstId: previousPageData.data.at(-1)?.id,
+ }]
+ }
- const appPrevChatList = useMemo(
- () => (currentConversationId && appChatListData?.data.length)
- ? getPrevChatList(appChatListData.data)
- : [],
- [appChatListData, currentConversationId],
+ return null
+ },
+ ([_, params]: [unknown, { conversationId: string; installedAppId: string; isInstalledApp: boolean; firstId: string }]) => {
+ if (params)
+ return fetchChatList(params.conversationId, params.isInstalledApp, params.installedAppId, params.firstId)
+ return null
+ },
+ { revalidateFirstPage: false },
)
+ const hasMore = useMemo(() => {
+ return !!(appChatListData && appChatListData[appChatListData?.length - 1]?.has_more)
+ }, [appChatListData])
+
+ const appPrevChatList = useMemo(() => {
+ const items = appChatListData?.flatMap((value: any) => value?.data || []) || []
+ const res = getPrevChatList(items)
+ return res
+ }, [appChatListData])
+
const [showNewConversationItemInList, setShowNewConversationItemInList] = useState(false)
const pinnedConversationList = useMemo(() => {
@@ -169,6 +208,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
}
})
}, [appParams])
+
useEffect(() => {
const conversationInputs: Record = {}
@@ -264,8 +304,9 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
setShowNewConversationItemInList(true)
}
}, [setShowConfigPanelBeforeChat, setShowNewConversationItemInList, checkInputsRequired])
+
const currentChatInstanceRef = useRef<{ handleStop: () => void }>({ handleStop: () => { } })
- const handleChangeConversation = useCallback((conversationId: string) => {
+ const handleChangeConversation = useCallback((conversationId: string, reloadCurrent = true) => {
currentChatInstanceRef.current.handleStop()
setNewConversationId('')
handleConversationIdInfoChange(conversationId)
@@ -274,21 +315,28 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
setShowConfigPanelBeforeChat(true)
else
setShowConfigPanelBeforeChat(false)
- }, [handleConversationIdInfoChange, setShowConfigPanelBeforeChat, checkInputsRequired])
- const handleNewConversation = useCallback(() => {
+
+ if (reloadCurrent)
+ mutateAppChatListData()
+ }, [handleConversationIdInfoChange, setShowConfigPanelBeforeChat, checkInputsRequired, mutateAppChatListData])
+
+ const handleNewConversation = useCallback((reloadCurrent = true) => {
currentChatInstanceRef.current.handleStop()
setNewConversationId('')
if (showNewConversationItemInList) {
- handleChangeConversation('')
+ handleChangeConversation('', reloadCurrent)
}
else if (currentConversationId) {
+ if (reloadCurrent)
+ mutateAppChatListData()
+
handleConversationIdInfoChange('')
setShowConfigPanelBeforeChat(true)
setShowNewConversationItemInList(true)
handleNewConversationInputsChange({})
}
- }, [handleChangeConversation, currentConversationId, handleConversationIdInfoChange, setShowConfigPanelBeforeChat, setShowNewConversationItemInList, showNewConversationItemInList, handleNewConversationInputsChange])
+ }, [handleChangeConversation, currentConversationId, handleConversationIdInfoChange, setShowConfigPanelBeforeChat, setShowNewConversationItemInList, showNewConversationItemInList, handleNewConversationInputsChange, mutateAppChatListData])
const handleUpdateConversationList = useCallback(() => {
mutateAppConversationData()
mutateAppPinnedConversationData()
@@ -327,7 +375,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
}
if (conversationId === currentConversationId)
- handleNewConversation()
+ handleNewConversation(false)
handleUpdateConversationList()
}, [isInstalledApp, appId, notify, t, handleUpdateConversationList, handleNewConversation, currentConversationId, conversationDeleting])
@@ -427,5 +475,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
chatShouldReloadKey,
handleFeedback,
currentChatInstanceRef,
+ hasMore,
+ setChatListSize,
}
}
diff --git a/web/app/components/base/chat/chat-with-history/index.tsx b/web/app/components/base/chat/chat-with-history/index.tsx
index 16524406d47d70..be94ec9b8bbc34 100644
--- a/web/app/components/base/chat/chat-with-history/index.tsx
+++ b/web/app/components/base/chat/chat-with-history/index.tsx
@@ -142,6 +142,8 @@ const ChatWithHistoryWrap: FC = ({
appId,
handleFeedback,
currentChatInstanceRef,
+ hasMore,
+ setChatListSize,
} = useChatWithHistory(installedAppInfo)
return (
@@ -178,6 +180,8 @@ const ChatWithHistoryWrap: FC = ({
handleFeedback,
currentChatInstanceRef,
themeBuilder,
+ hasMore,
+ setChatListSize,
}}>
diff --git a/web/app/components/base/chat/chat/hooks.ts b/web/app/components/base/chat/chat/hooks.ts
index fa923ca0094978..3adb8b0639f1ff 100644
--- a/web/app/components/base/chat/chat/hooks.ts
+++ b/web/app/components/base/chat/chat/hooks.ts
@@ -75,6 +75,14 @@ export const useChat = (
setChatList(newChatList)
chatListRef.current = newChatList
}, [])
+
+ useEffect(() => {
+ if (prevChatList && prevChatList.length > 0) {
+ setChatList(prevChatList)
+ chatListRef.current = prevChatList
+ }
+ }, [prevChatList])
+
const handleResponding = useCallback((isResponding: boolean) => {
setIsResponding(isResponding)
isRespondingRef.current = isResponding
@@ -249,7 +257,7 @@ export const useChat = (
else
ttsUrl = `/apps/${params.appId}/text-to-audio`
}
- const player = AudioPlayerManager.getInstance().getAudioPlayer(ttsUrl, ttsIsPublic, uuidV4(), 'none', 'none', (_: any): any => {})
+ const player = AudioPlayerManager.getInstance().getAudioPlayer(ttsUrl, ttsIsPublic, uuidV4(), 'none', 'none', (_: any): any => { })
ssePost(
url,
{
diff --git a/web/app/components/base/chat/chat/index.tsx b/web/app/components/base/chat/chat/index.tsx
index e6de01252dbd09..b3b1689fec4ccb 100644
--- a/web/app/components/base/chat/chat/index.tsx
+++ b/web/app/components/base/chat/chat/index.tsx
@@ -10,7 +10,7 @@ import {
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
-import { debounce } from 'lodash-es'
+import { debounce, throttle } from 'lodash-es'
import { useShallow } from 'zustand/react/shallow'
import type {
ChatConfig,
@@ -70,6 +70,8 @@ export type ChatProps = {
showFileUpload?: boolean
onFeatureBarClick?: (state: boolean) => void
noSpacing?: boolean
+ onScrollToTop?: () => void
+ hasMoreMessages?: boolean
}
const Chat: FC = ({
@@ -106,6 +108,8 @@ const Chat: FC = ({
showFileUpload,
onFeatureBarClick,
noSpacing,
+ onScrollToTop,
+ hasMoreMessages,
}) => {
const { t } = useTranslation()
const { currentLogItem, setCurrentLogItem, showPromptLogModal, setShowPromptLogModal, showAgentLogModal, setShowAgentLogModal } = useAppStore(useShallow(state => ({
@@ -122,12 +126,40 @@ const Chat: FC = ({
const chatFooterRef = useRef(null)
const chatFooterInnerRef = useRef(null)
const userScrolledRef = useRef(false)
+ const lastScrollTopRef = useRef(0)
+ const haveMoreMessagesRef = useRef(hasMoreMessages)
+
+ useEffect(() => {
+ haveMoreMessagesRef.current = hasMoreMessages
+ }, [hasMoreMessages])
const handleScrollToBottom = useCallback(() => {
if (chatList.length > 1 && chatContainerRef.current && !userScrolledRef.current)
chatContainerRef.current.scrollTop = chatContainerRef.current.scrollHeight
}, [chatList.length])
+ const handleScrollToTop = useCallback(() => {
+ const handleScroll = throttle(async () => {
+ if (haveMoreMessagesRef.current && chatContainerRef.current) {
+ const currentScrollTop = chatContainerRef.current.scrollTop
+ const isScrollingUp = currentScrollTop < lastScrollTopRef.current
+ if (isScrollingUp && currentScrollTop < 50) {
+ if (chatContainerRef.current)
+ chatContainerRef.current.scrollTop = 100
+
+ chatContainerRef.current.style.overflowY = 'hidden'
+ onScrollToTop && onScrollToTop()
+ chatContainerRef.current.style.overflowY = 'auto'
+ }
+ lastScrollTopRef.current = currentScrollTop
+ }
+ }, 1000)
+ chatContainerRef.current?.addEventListener('scroll', handleScroll)
+ return () => {
+ chatContainerRef.current?.removeEventListener('scroll', handleScroll)
+ }
+ }, [onScrollToTop])
+
const handleWindowResize = useCallback(() => {
if (chatContainerRef.current)
setWidth(document.body.clientWidth - (chatContainerRef.current?.clientWidth + 16) - 8)
@@ -142,13 +174,15 @@ const Chat: FC = ({
useEffect(() => {
handleScrollToBottom()
handleWindowResize()
- }, [handleScrollToBottom, handleWindowResize])
+ handleScrollToTop()
+ }, [handleScrollToBottom, handleWindowResize, handleScrollToTop])
useEffect(() => {
if (chatContainerRef.current) {
requestAnimationFrame(() => {
handleScrollToBottom()
handleWindowResize()
+ handleScrollToTop()
})
}
})
@@ -216,6 +250,14 @@ const Chat: FC = ({
ref={chatContainerInnerRef}
className={cn('w-full', !noSpacing && 'px-8', chatContainerInnerClassName)}
>
+ {
+ !hasMoreMessages && chatList.length > 20
+ &&
+
+ {t('share.chat.loadedAllMessages')}
+
+
+ }
{
chatList.map((item, index) => {
if (item.isAnswer) {
diff --git a/web/i18n/en-US/share-app.ts b/web/i18n/en-US/share-app.ts
index 5d47fd31cf85eb..f643647168b480 100644
--- a/web/i18n/en-US/share-app.ts
+++ b/web/i18n/en-US/share-app.ts
@@ -30,6 +30,7 @@ const translation = {
},
tryToSolve: 'Try to solve',
temporarySystemIssue: 'Sorry, temporary system issue.',
+ loadedAllMessages: 'Loaded all messages',
},
generation: {
tabs: {
diff --git a/web/i18n/zh-Hans/share-app.ts b/web/i18n/zh-Hans/share-app.ts
index 968381bb3714cb..de8792cd97afa9 100644
--- a/web/i18n/zh-Hans/share-app.ts
+++ b/web/i18n/zh-Hans/share-app.ts
@@ -26,6 +26,7 @@ const translation = {
},
tryToSolve: '尝试解决',
temporarySystemIssue: '抱歉,临时系统问题。',
+ loadedAllMessages: '已加载所有消息',
},
generation: {
tabs: {
diff --git a/web/service/share.ts b/web/service/share.ts
index 0e46e30d01fec7..c61777165ac905 100644
--- a/web/service/share.ts
+++ b/web/service/share.ts
@@ -130,8 +130,14 @@ export const generationConversationName = async (isInstalledApp: boolean, instal
return getAction('post', isInstalledApp)(getUrl(`conversations/${id}/name`, isInstalledApp, installedAppId), { body: { auto_generate: true } }) as Promise
}
-export const fetchChatList = async (conversationId: string, isInstalledApp: boolean, installedAppId = '') => {
- return getAction('get', isInstalledApp)(getUrl('messages', isInstalledApp, installedAppId), { params: { conversation_id: conversationId, limit: 20, last_id: '' } }) as any
+export const fetchChatList = async (conversationId: string, isInstalledApp: boolean, installedAppId = '', firstId = '') => {
+ return getAction('get', isInstalledApp)(getUrl('messages', isInstalledApp, installedAppId), {
+ params: {
+ conversation_id: conversationId,
+ limit: 20,
+ first_id: firstId,
+ },
+ }) as any
}
// Abandoned API interface