From f2b2effc4b32dba7b3a4daf2065db5d00a97bf69 Mon Sep 17 00:00:00 2001 From: zxhlyh Date: Thu, 25 Jan 2024 14:55:12 +0800 Subject: [PATCH] fix: typing delay (#2200) --- web/app/components/app/chat/answer/index.tsx | 16 +- web/app/components/app/chat/index.tsx | 149 +++++++-------- .../components/app/chat/question/index.tsx | 4 +- .../share/chat/hooks/use-conversation.ts | 6 +- web/app/components/share/chat/index.tsx | 174 ++++++++++-------- .../components/share/chat/sidebar/index.tsx | 13 +- 6 files changed, 201 insertions(+), 161 deletions(-) diff --git a/web/app/components/app/chat/answer/index.tsx b/web/app/components/app/chat/answer/index.tsx index 7c13a17f66..05ea6f9d89 100644 --- a/web/app/components/app/chat/answer/index.tsx +++ b/web/app/components/app/chat/answer/index.tsx @@ -43,6 +43,7 @@ const IconWrapper: FC<{ children: React.ReactNode | string }> = ({ children }) = } export type IAnswerProps = { item: IChatItem + index: number feedbackDisabled: boolean isHideFeedbackEdit: boolean onQueryChange: (query: string) => void @@ -59,14 +60,15 @@ export type IAnswerProps = { supportAnnotation?: boolean appId?: string question: string - onAnnotationEdited?: (question: string, answer: string) => void - onAnnotationAdded?: (annotationId: string, authorName: string, question: string, answer: string) => void - onAnnotationRemoved?: () => void + onAnnotationEdited?: (question: string, answer: string, index: number) => void + onAnnotationAdded?: (annotationId: string, authorName: string, question: string, answer: string, index: number) => void + onAnnotationRemoved?: (index: number) => void allToolIcons?: Record } // The component needs to maintain its own state to control whether to display input component const Answer: FC = ({ item, + index, onQueryChange, feedbackDisabled = false, isHideFeedbackEdit = false, @@ -340,9 +342,9 @@ const Answer: FC = ({ cached={hasAnnotation} query={question} answer={content} - onAdded={(id, authorName) => onAnnotationAdded?.(id, authorName, question, content)} + onAdded={(id, authorName) => onAnnotationAdded?.(id, authorName, question, content, index)} onEdit={() => setIsShowReplyModal(true)} - onRemoved={onAnnotationRemoved!} + onRemoved={() => onAnnotationRemoved!(index)} /> )} @@ -351,8 +353,8 @@ const Answer: FC = ({ onHide={() => setIsShowReplyModal(false)} query={question} answer={content} - onEdited={onAnnotationEdited!} - onAdded={onAnnotationAdded!} + onEdited={(editedQuery, editedAnswer) => onAnnotationEdited!(editedQuery, editedAnswer, index)} + onAdded={(annotationId, authorName, editedQuery, editedAnswer) => onAnnotationAdded!(annotationId, authorName, editedQuery, editedAnswer, index)} appId={appId!} messageId={id} annotationId={annotation?.id || ''} diff --git a/web/app/components/app/chat/index.tsx b/web/app/components/app/chat/index.tsx index 95dbe3a72d..c683416b57 100644 --- a/web/app/components/app/chat/index.tsx +++ b/web/app/components/app/chat/index.tsx @@ -1,6 +1,6 @@ 'use client' import type { FC, ReactNode } from 'react' -import React, { useEffect, useLayoutEffect, useRef, useState } from 'react' +import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react' import Textarea from 'rc-textarea' import { useContext } from 'use-context-selector' import cn from 'classnames' @@ -197,6 +197,76 @@ const Chat: FC = ({ logError(t('common.voiceInput.notAllow')) }) } + const handleQueryChangeFromAnswer = useCallback((val: string) => { + onQueryChange(val) + handleSend(val) + }, []) + const handleAnnotationEdited = useCallback((query: string, answer: string, index: number) => { + onChatListChange?.(chatList.map((item, i) => { + if (i === index - 1) { + return { + ...item, + content: query, + } + } + if (i === index) { + return { + ...item, + content: answer, + annotation: { + ...item.annotation, + logAnnotation: undefined, + } as any, + } + } + return item + })) + }, []) + const handleAnnotationAdded = useCallback((annotationId: string, authorName: string, query: string, answer: string, index: number) => { + onChatListChange?.(chatList.map((item, i) => { + if (i === index - 1) { + return { + ...item, + content: query, + } + } + if (i === index) { + const answerItem = { + ...item, + content: item.content, + annotation: { + id: annotationId, + authorName, + logAnnotation: { + content: answer, + account: { + id: '', + name: authorName, + email: '', + }, + }, + } as Annotation, + } + return answerItem + } + return item + })) + }, []) + const handleAnnotationRemoved = useCallback((index: number) => { + onChatListChange?.(chatList.map((item, i) => { + if (i === index) { + return { + ...item, + content: item.content, + annotation: { + ...(item.annotation || {}), + id: '', + } as Annotation, + } + } + return item + })) + }, []) return (
@@ -210,10 +280,8 @@ const Chat: FC = ({ return { - onQueryChange(val) - handleSend(val) - }} + index={index} + onQueryChange={handleQueryChangeFromAnswer} feedbackDisabled={feedbackDisabled} isHideFeedbackEdit={isHideFeedbackEdit} onFeedback={onFeedback} @@ -228,72 +296,9 @@ const Chat: FC = ({ supportAnnotation={supportAnnotation} appId={appId} question={chatList[index - 1]?.content} - onAnnotationEdited={(query, answer) => { - onChatListChange?.(chatList.map((item, i) => { - if (i === index - 1) { - return { - ...item, - content: query, - } - } - if (i === index) { - return { - ...item, - content: answer, - annotation: { - ...item.annotation, - logAnnotation: undefined, - } as any, - } - } - return item - })) - }} - onAnnotationAdded={(annotationId, authorName, query, answer) => { - onChatListChange?.(chatList.map((item, i) => { - if (i === index - 1) { - return { - ...item, - content: query, - } - } - if (i === index) { - const answerItem = { - ...item, - content: item.content, - annotation: { - id: annotationId, - authorName, - logAnnotation: { - content: answer, - account: { - id: '', - name: authorName, - email: '', - }, - }, - } as Annotation, - } - return answerItem - } - return item - })) - }} - onAnnotationRemoved={() => { - onChatListChange?.(chatList.map((item, i) => { - if (i === index) { - return { - ...item, - content: item.content, - annotation: { - ...(item.annotation || {}), - id: '', - } as Annotation, - } - } - return item - })) - }} + onAnnotationEdited={handleAnnotationEdited} + onAnnotationAdded={handleAnnotationAdded} + onAnnotationRemoved={handleAnnotationRemoved} allToolIcons={allToolIcons} /> } @@ -307,8 +312,6 @@ const Chat: FC = ({ item={item} isShowPromptLog={isShowPromptLog} isResponsing={isResponsing} - // ['https://placekitten.com/360/360', 'https://placekitten.com/360/640'] - imgSrcs={(item.message_files && item.message_files?.length > 0) ? item.message_files.map(item => item.url) : []} /> ) })} diff --git a/web/app/components/app/chat/question/index.tsx b/web/app/components/app/chat/question/index.tsx index b2748c447a..e5806709ea 100644 --- a/web/app/components/app/chat/question/index.tsx +++ b/web/app/components/app/chat/question/index.tsx @@ -13,14 +13,14 @@ import ImageGallery from '@/app/components/base/image-gallery' type IQuestionProps = Pick & { isShowPromptLog?: boolean item: IChatItem - imgSrcs?: string[] isResponsing?: boolean } -const Question: FC = ({ id, content, imgSrcs, more, useCurrentUserAvatar, isShowPromptLog, item, isResponsing }) => { +const Question: FC = ({ id, content, more, useCurrentUserAvatar, isShowPromptLog, item, isResponsing }) => { const { userProfile } = useContext(AppContext) const userName = userProfile?.name const ref = useRef(null) + const imgSrcs = item.message_files?.map(item => item.url) return (
diff --git a/web/app/components/share/chat/hooks/use-conversation.ts b/web/app/components/share/chat/hooks/use-conversation.ts index 9b797a2474..00503500ec 100644 --- a/web/app/components/share/chat/hooks/use-conversation.ts +++ b/web/app/components/share/chat/hooks/use-conversation.ts @@ -1,4 +1,4 @@ -import { useState } from 'react' +import { useCallback, useState } from 'react' import produce from 'immer' import { useGetState } from 'ahooks' import type { ConversationItem } from '@/models/share' @@ -11,7 +11,7 @@ function useConversation() { const [pinnedConversationList, setPinnedConversationList] = useState([]) const [currConversationId, doSetCurrConversationId, getCurrConversationId] = useGetState('-1') // when set conversation id, we do not have set appId - const setCurrConversationId = (id: string, appId: string, isSetToLocalStroge = true, newConversationName = '') => { + const setCurrConversationId = useCallback((id: string, appId: string, isSetToLocalStroge = true, newConversationName = '') => { doSetCurrConversationId(id) if (isSetToLocalStroge && id !== '-1') { // conversationIdInfo: {[appId1]: conversationId1, [appId2]: conversationId2} @@ -19,7 +19,7 @@ function useConversation() { conversationIdInfo[appId] = id globalThis.localStorage?.setItem(storageConversationIdKey, JSON.stringify(conversationIdInfo)) } - } + }, [doSetCurrConversationId]) const getConversationIdFromStorage = (appId: string) => { const conversationIdInfo = globalThis.localStorage?.getItem(storageConversationIdKey) ? JSON.parse(globalThis.localStorage?.getItem(storageConversationIdKey) || '') : {} diff --git a/web/app/components/share/chat/index.tsx b/web/app/components/share/chat/index.tsx index 7cd4aaceaa..ed0d9f5c34 100644 --- a/web/app/components/share/chat/index.tsx +++ b/web/app/components/share/chat/index.tsx @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-use-before-define */ 'use client' import type { FC } from 'react' -import React, { useEffect, useRef, useState } from 'react' +import React, { useCallback, useEffect, useRef, useState } from 'react' import cn from 'classnames' import useSWR from 'swr' import { useTranslation } from 'react-i18next' @@ -65,6 +65,7 @@ const Main: FC = ({ installedAppInfo, }) => { const { t } = useTranslation() + const { notify } = useContext(ToastContext) const media = useBreakpoints() const isMobile = media === MediaType.mobile @@ -123,7 +124,8 @@ const Main: FC = ({ const [suggestedQuestions, setSuggestQuestions] = useState([]) const [hasMore, setHasMore] = useState(true) const [hasPinnedMore, setHasPinnedMore] = useState(true) - const onMoreLoaded = ({ data: conversations, has_more }: any) => { + const [isShowSuggestion, setIsShowSuggestion] = useState(false) + const onMoreLoaded = useCallback(({ data: conversations, has_more }: any) => { setHasMore(has_more) if (isClearConversationList) { setConversationList(conversations) @@ -132,8 +134,8 @@ const Main: FC = ({ else { setConversationList([...conversationList, ...conversations]) } - } - const onPinnedMoreLoaded = ({ data: conversations, has_more }: any) => { + }, [conversationList, setConversationList, isClearConversationList, clearConversationListFalse]) + const onPinnedMoreLoaded = useCallback(({ data: conversations, has_more }: any) => { setHasPinnedMore(has_more) if (isClearPinnedConversationList) { setPinnedConversationList(conversations) @@ -142,9 +144,9 @@ const Main: FC = ({ else { setPinnedConversationList([...pinnedConversationList, ...conversations]) } - } + }, [pinnedConversationList, setPinnedConversationList, isClearPinnedConversationList, clearPinnedConversationListFalse]) const [controlUpdateConversationList, setControlUpdateConversationList] = useState(0) - const noticeUpdateList = () => { + const noticeUpdateList = useCallback(() => { setHasMore(true) clearConversationListTrue() @@ -152,25 +154,25 @@ const Main: FC = ({ clearPinnedConversationListTrue() setControlUpdateConversationList(Date.now()) - } - const handlePin = async (id: string) => { + }, [clearConversationListTrue, clearPinnedConversationListTrue]) + const handlePin = useCallback(async (id: string) => { await pinConversation(isInstalledApp, installedAppInfo?.id, id) notify({ type: 'success', message: t('common.api.success') }) noticeUpdateList() - } + }, [isInstalledApp, installedAppInfo?.id, t, notify, noticeUpdateList]) - const handleUnpin = async (id: string) => { + const handleUnpin = useCallback(async (id: string) => { await unpinConversation(isInstalledApp, installedAppInfo?.id, id) notify({ type: 'success', message: t('common.api.success') }) noticeUpdateList() - } + }, [isInstalledApp, installedAppInfo?.id, t, notify, noticeUpdateList]) const [isShowConfirm, { setTrue: showConfirm, setFalse: hideConfirm }] = useBoolean(false) const [toDeleteConversationId, setToDeleteConversationId] = useState('') - const handleDelete = (id: string) => { + const handleDelete = useCallback((id: string) => { setToDeleteConversationId(id) hideSidebar() // mobile showConfirm() - } + }, [hideSidebar, showConfirm]) const didDelete = async () => { await delConversation(isInstalledApp, installedAppInfo?.id, toDeleteConversationId) @@ -186,17 +188,51 @@ const Main: FC = ({ const [speechToTextConfig, setSpeechToTextConfig] = useState(null) const [textToSpeechConfig, setTextToSpeechConfig] = useState(null) const [citationConfig, setCitationConfig] = useState(null) - + const [chatList, setChatList, getChatList] = useGetState([]) + const chatListDomRef = useRef(null) + const [isResponsing, { setTrue: setResponsingTrue, setFalse: setResponsingFalse }] = useBoolean(false) + const [abortController, setAbortController] = useState(null) const [conversationIdChangeBecauseOfNew, setConversationIdChangeBecauseOfNew, getConversationIdChangeBecauseOfNew] = useGetState(false) const [isChatStarted, { setTrue: setChatStarted, setFalse: setChatNotStarted }] = useBoolean(false) - const handleStartChat = (inputs: Record) => { + const conversationIntroduction = currConversationInfo?.introduction || '' + const createNewChat = useCallback(async () => { + // if new chat is already exist, do not create new chat + abortController?.abort() + setResponsingFalse() + if (conversationList.some(item => item.id === '-1')) + return + + setConversationList(produce(conversationList, (draft) => { + draft.unshift({ + id: '-1', + name: t('share.chat.newChatDefaultName'), + inputs: newConversationInputs, + introduction: conversationIntroduction, + }) + })) + }, [ + abortController, + setResponsingFalse, + setConversationList, + conversationList, + newConversationInputs, + conversationIntroduction, + t, + ]) + const handleStartChat = useCallback((inputs: Record) => { createNewChat() setConversationIdChangeBecauseOfNew(true) setCurrInputs(inputs) setChatStarted() // parse variables in introduction setChatList(generateNewChatListWithOpenstatement('', inputs)) - } + }, [ + createNewChat, + setConversationIdChangeBecauseOfNew, + setCurrInputs, + setChatStarted, + setChatList, + ]) const hasSetInputs = (() => { if (!isNewConversation) return true @@ -205,7 +241,6 @@ const Main: FC = ({ })() const conversationName = currConversationInfo?.name || t('share.chat.newChatDefaultName') as string - const conversationIntroduction = currConversationInfo?.introduction || '' const [controlChatUpdateAllConversation, setControlChatUpdateAllConversation] = useState(0) // onData change thought (the produce obj). https://github.com/immerjs/immer/issues/576 @@ -293,7 +328,18 @@ const Main: FC = ({ } useEffect(handleConversationSwitch, [currConversationId, inited]) - const handleConversationIdChange = (id: string) => { + /* + * chat info. chat is under conversation. + */ + useEffect(() => { + // scroll to bottom + if (chatListDomRef.current) + chatListDomRef.current.scrollTop = chatListDomRef.current.scrollHeight + }, [chatList, currConversationId]) + // user can not edit inputs if user had send message + const canEditInpus = !chatList.some(item => item.isAnswer === false) && isNewConversation + + const handleConversationIdChange = useCallback((id: string) => { if (id === '-1') { createNewChat() setConversationIdChangeBecauseOfNew(true) @@ -305,36 +351,14 @@ const Main: FC = ({ setCurrConversationId(id, appId) setIsShowSuggestion(false) hideSidebar() - } - - /* - * chat info. chat is under conversation. - */ - const [chatList, setChatList, getChatList] = useGetState([]) - const chatListDomRef = useRef(null) - useEffect(() => { - // scroll to bottom - if (chatListDomRef.current) - chatListDomRef.current.scrollTop = chatListDomRef.current.scrollHeight - }, [chatList, currConversationId]) - // user can not edit inputs if user had send message - const canEditInpus = !chatList.some(item => item.isAnswer === false) && isNewConversation - const createNewChat = async () => { - // if new chat is already exist, do not create new chat - abortController?.abort() - setResponsingFalse() - if (conversationList.some(item => item.id === '-1')) - return - - setConversationList(produce(conversationList, (draft) => { - draft.unshift({ - id: '-1', - name: t('share.chat.newChatDefaultName'), - inputs: newConversationInputs, - introduction: conversationIntroduction, - }) - })) - } + }, [ + appId, + createNewChat, + hideSidebar, + setCurrConversationId, + setIsShowSuggestion, + setConversationIdChangeBecauseOfNew, + ]) // sometime introduction is not applied to state const generateNewChatListWithOpenstatement = (introduction?: string, inputs?: Record | null) => { @@ -446,14 +470,11 @@ const Main: FC = ({ })() }, []) - const [isResponsing, { setTrue: setResponsingTrue, setFalse: setResponsingFalse }] = useBoolean(false) - const [abortController, setAbortController] = useState(null) - const { notify } = useContext(ToastContext) - const logError = (message: string) => { + const logError = useCallback((message: string) => { notify({ type: 'error', message }) - } + }, [notify]) - const checkCanSend = () => { + const checkCanSend = useCallback(() => { if (currConversationId !== '-1') return true @@ -480,10 +501,9 @@ const Main: FC = ({ return false } return !hasEmptyInput - } + }, [currConversationId, currInputs, promptConfig, t, logError]) const [controlFocus, setControlFocus] = useState(0) - const [isShowSuggestion, setIsShowSuggestion] = useState(false) const doShowSuggestion = isShowSuggestion && !isResponsing const [openingSuggestedQuestions, setOpeningSuggestedQuestions] = useState([]) const [messageTaskId, setMessageTaskId] = useState('') @@ -755,7 +775,7 @@ const Main: FC = ({ }, isInstalledApp, installedAppInfo?.id) } - const handleFeedback = async (messageId: string, feedback: Feedbacktype) => { + const handleFeedback = useCallback(async (messageId: string, feedback: Feedbacktype) => { await updateFeedback({ url: `/messages/${messageId}/feedbacks`, body: { rating: feedback.rating } }, isInstalledApp, installedAppInfo?.id) const newChatList = chatList.map((item) => { if (item.id === messageId) { @@ -768,7 +788,19 @@ const Main: FC = ({ }) setChatList(newChatList) notify({ type: 'success', message: t('common.api.success') }) - } + }, [isInstalledApp, installedAppInfo?.id, chatList, t, notify, setChatList]) + + const handleListChanged = useCallback((list: ConversationItem[]) => { + setConversationList(list) + setControlChatUpdateAllConversation(Date.now()) + }, [setConversationList, setControlChatUpdateAllConversation]) + const handlePinnedListChanged = useCallback((list: ConversationItem[]) => { + setPinnedConversationList(list) + setControlChatUpdateAllConversation(Date.now()) + }, [setPinnedConversationList, setControlChatUpdateAllConversation]) + const handleStartChatOnSidebar = useCallback(() => { + handleConversationIdChange('-1') + }, [handleConversationIdChange]) const renderSidebar = () => { if (!appId || !siteInfo || !promptConfig) @@ -776,16 +808,10 @@ const Main: FC = ({ return ( { - setConversationList(list) - setControlChatUpdateAllConversation(Date.now()) - }} + onListChanged={handleListChanged} isClearConversationList={isClearConversationList} pinnedList={pinnedConversationList} - onPinnedListChanged={(list) => { - setPinnedConversationList(list) - setControlChatUpdateAllConversation(Date.now()) - }} + onPinnedListChanged={handlePinnedListChanged} isClearPinnedConversationList={isClearPinnedConversationList} onMoreLoaded={onMoreLoaded} onPinnedMoreLoaded={onPinnedMoreLoaded} @@ -801,11 +827,17 @@ const Main: FC = ({ onUnpin={handleUnpin} controlUpdateList={controlUpdateConversationList} onDelete={handleDelete} - onStartChat={() => handleConversationIdChange('-1')} + onStartChat={handleStartChatOnSidebar} /> ) } + const handleAbortResponsing = useCallback(async () => { + await stopChatMessageResponding(appId, messageTaskId, isInstalledApp, installedAppInfo?.id) + setHasStopResponded(true) + setResponsingFalse() + }, [appId, messageTaskId, isInstalledApp, installedAppInfo?.id]) + if (appUnavailable) return @@ -824,7 +856,7 @@ const Main: FC = ({ icon_background={siteInfo.icon_background} isMobile={isMobile} onShowSideBar={showSidebar} - onCreateNewChat={() => handleConversationIdChange('-1')} + onCreateNewChat={handleStartChatOnSidebar} /> )} @@ -884,11 +916,7 @@ const Main: FC = ({ onFeedback={handleFeedback} isResponsing={isResponsing} canStopResponsing={!!messageTaskId && isResponsingConIsCurrCon} - abortResponsing={async () => { - await stopChatMessageResponding(appId, messageTaskId, isInstalledApp, installedAppInfo?.id) - setHasStopResponded(true) - setResponsingFalse() - }} + abortResponsing={handleAbortResponsing} checkCanSend={checkCanSend} controlFocus={controlFocus} isShowSuggestion={doShowSuggestion} diff --git a/web/app/components/share/chat/sidebar/index.tsx b/web/app/components/share/chat/sidebar/index.tsx index 7fd4afeb5d..d216d62eb4 100644 --- a/web/app/components/share/chat/sidebar/index.tsx +++ b/web/app/components/share/chat/sidebar/index.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react' +import React, { useCallback, useEffect, useState } from 'react' import type { FC } from 'react' import { useTranslation } from 'react-i18next' import { @@ -76,6 +76,13 @@ const Sidebar: FC = ({ checkHasPinned() }, [controlUpdateList]) + const handleUnpin = useCallback((id: string) => { + onUnpin(id) + }, [onUnpin]) + const handlePin = useCallback((id: string) => { + onPin(id) + }, [onPin]) + const maxListHeight = (isInstalledApp) ? 'max-h-[30vh]' : 'max-h-[40vh]' return ( @@ -119,7 +126,7 @@ const Sidebar: FC = ({ onMoreLoaded={onPinnedMoreLoaded} isNoMore={isPinnedNoMore} isPinned={true} - onPinChanged={id => onUnpin(id)} + onPinChanged={handleUnpin} controlUpdate={controlUpdateList + 1} onDelete={onDelete} /> @@ -142,7 +149,7 @@ const Sidebar: FC = ({ onMoreLoaded={onMoreLoaded} isNoMore={isNoMore} isPinned={false} - onPinChanged={id => onPin(id)} + onPinChanged={handlePin} controlUpdate={controlUpdateList + 1} onDelete={onDelete} />