Skip to content
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

Add "Scroll Up to Load More" and Fix Old Replies Issue in Chat Component. #12348

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion web/app/components/base/chat/chat-with-history/chat-wrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ const ChatWrapper = () => {
currentChatInstanceRef,
appData,
themeBuilder,
setChatListSize,
hasMore,
} = useChatWithHistoryContext()
const appConfig = useMemo(() => {
const config = appParams || {}
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -163,6 +165,11 @@ const ChatWrapper = () => {
/>
: null

const onScrollToTop = useCallback(() => {
if (hasMore)
setChatListSize(size => size + 1)
}, [setChatListSize, hasMore])

return (
<div
className='h-full bg-chatbot-bg overflow-hidden'
Expand All @@ -187,6 +194,8 @@ const ChatWrapper = () => {
answerIcon={answerIcon}
hideProcessDetail
themeBuilder={themeBuilder}
onScrollToTop={onScrollToTop}
hasMoreMessages={hasMore}
/>
</div>
)
Expand Down
26 changes: 15 additions & 11 deletions web/app/components/base/chat/chat-with-history/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<ChatWithHistoryContextValue>({
Expand All @@ -59,21 +61,23 @@ export const ChatWithHistoryContext = createContext<ChatWithHistoryContextValue>
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)
74 changes: 62 additions & 12 deletions web/app/components/base/chat/chat-with-history/hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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,
Expand All @@ -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<IChatItem>; 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)
Expand Down Expand Up @@ -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<IChatWithHistory | null>(
(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(() => {
Expand Down Expand Up @@ -169,6 +208,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
}
})
}, [appParams])

useEffect(() => {
const conversationInputs: Record<string, any> = {}

Expand Down Expand Up @@ -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)
Expand All @@ -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()
Expand Down Expand Up @@ -327,7 +375,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
}

if (conversationId === currentConversationId)
handleNewConversation()
handleNewConversation(false)

handleUpdateConversationList()
}, [isInstalledApp, appId, notify, t, handleUpdateConversationList, handleNewConversation, currentConversationId, conversationDeleting])
Expand Down Expand Up @@ -427,5 +475,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
chatShouldReloadKey,
handleFeedback,
currentChatInstanceRef,
hasMore,
setChatListSize,
}
}
4 changes: 4 additions & 0 deletions web/app/components/base/chat/chat-with-history/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,8 @@ const ChatWithHistoryWrap: FC<ChatWithHistoryWrapProps> = ({
appId,
handleFeedback,
currentChatInstanceRef,
hasMore,
setChatListSize,
} = useChatWithHistory(installedAppInfo)

return (
Expand Down Expand Up @@ -178,6 +180,8 @@ const ChatWithHistoryWrap: FC<ChatWithHistoryWrapProps> = ({
handleFeedback,
currentChatInstanceRef,
themeBuilder,
hasMore,
setChatListSize,
}}>
<ChatWithHistory className={className} />
</ChatWithHistoryContext.Provider>
Expand Down
10 changes: 9 additions & 1 deletion web/app/components/base/chat/chat/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
{
Expand Down
46 changes: 44 additions & 2 deletions web/app/components/base/chat/chat/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -70,6 +70,8 @@ export type ChatProps = {
showFileUpload?: boolean
onFeatureBarClick?: (state: boolean) => void
noSpacing?: boolean
onScrollToTop?: () => void
hasMoreMessages?: boolean
}

const Chat: FC<ChatProps> = ({
Expand Down Expand Up @@ -106,6 +108,8 @@ const Chat: FC<ChatProps> = ({
showFileUpload,
onFeatureBarClick,
noSpacing,
onScrollToTop,
hasMoreMessages,
}) => {
const { t } = useTranslation()
const { currentLogItem, setCurrentLogItem, showPromptLogModal, setShowPromptLogModal, showAgentLogModal, setShowAgentLogModal } = useAppStore(useShallow(state => ({
Expand All @@ -122,12 +126,40 @@ const Chat: FC<ChatProps> = ({
const chatFooterRef = useRef<HTMLDivElement>(null)
const chatFooterInnerRef = useRef<HTMLDivElement>(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)
Expand All @@ -142,13 +174,15 @@ const Chat: FC<ChatProps> = ({
useEffect(() => {
handleScrollToBottom()
handleWindowResize()
}, [handleScrollToBottom, handleWindowResize])
handleScrollToTop()
}, [handleScrollToBottom, handleWindowResize, handleScrollToTop])

useEffect(() => {
if (chatContainerRef.current) {
requestAnimationFrame(() => {
handleScrollToBottom()
handleWindowResize()
handleScrollToTop()
})
}
})
Expand Down Expand Up @@ -216,6 +250,14 @@ const Chat: FC<ChatProps> = ({
ref={chatContainerInnerRef}
className={cn('w-full', !noSpacing && 'px-8', chatContainerInnerClassName)}
>
{
!hasMoreMessages && chatList.length > 20
&& <div className="flex justify-center content-center w-full mb-6">
<div className="text-gray-500 text-sm text-center my-4 relative">
{t('share.chat.loadedAllMessages')}
</div>
</div>
}
{
chatList.map((item, index) => {
if (item.isAnswer) {
Expand Down
1 change: 1 addition & 0 deletions web/i18n/en-US/share-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const translation = {
},
tryToSolve: 'Try to solve',
temporarySystemIssue: 'Sorry, temporary system issue.',
loadedAllMessages: 'Loaded all messages',
},
generation: {
tabs: {
Expand Down
1 change: 1 addition & 0 deletions web/i18n/zh-Hans/share-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const translation = {
},
tryToSolve: '尝试解决',
temporarySystemIssue: '抱歉,临时系统问题。',
loadedAllMessages: '已加载所有消息',
},
generation: {
tabs: {
Expand Down
Loading
Loading