|
|
|
@@ -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<IMainProps> = ({ |
|
|
|
installedAppInfo, |
|
|
|
}) => { |
|
|
|
const { t } = useTranslation() |
|
|
|
const { notify } = useContext(ToastContext) |
|
|
|
const media = useBreakpoints() |
|
|
|
const isMobile = media === MediaType.mobile |
|
|
|
|
|
|
|
@@ -123,7 +124,8 @@ const Main: FC<IMainProps> = ({ |
|
|
|
const [suggestedQuestions, setSuggestQuestions] = useState<string[]>([]) |
|
|
|
const [hasMore, setHasMore] = useState<boolean>(true) |
|
|
|
const [hasPinnedMore, setHasPinnedMore] = useState<boolean>(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<IMainProps> = ({ |
|
|
|
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<IMainProps> = ({ |
|
|
|
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<IMainProps> = ({ |
|
|
|
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<IMainProps> = ({ |
|
|
|
const [speechToTextConfig, setSpeechToTextConfig] = useState<SpeechToTextConfig | null>(null) |
|
|
|
const [textToSpeechConfig, setTextToSpeechConfig] = useState<TextToSpeechConfig | null>(null) |
|
|
|
const [citationConfig, setCitationConfig] = useState<CitationConfig | null>(null) |
|
|
|
|
|
|
|
const [chatList, setChatList, getChatList] = useGetState<IChatItem[]>([]) |
|
|
|
const chatListDomRef = useRef<HTMLDivElement>(null) |
|
|
|
const [isResponsing, { setTrue: setResponsingTrue, setFalse: setResponsingFalse }] = useBoolean(false) |
|
|
|
const [abortController, setAbortController] = useState<AbortController | null>(null) |
|
|
|
const [conversationIdChangeBecauseOfNew, setConversationIdChangeBecauseOfNew, getConversationIdChangeBecauseOfNew] = useGetState(false) |
|
|
|
const [isChatStarted, { setTrue: setChatStarted, setFalse: setChatNotStarted }] = useBoolean(false) |
|
|
|
const handleStartChat = (inputs: Record<string, any>) => { |
|
|
|
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<string, any>) => { |
|
|
|
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<IMainProps> = ({ |
|
|
|
})() |
|
|
|
|
|
|
|
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,25 +328,9 @@ const Main: FC<IMainProps> = ({ |
|
|
|
} |
|
|
|
useEffect(handleConversationSwitch, [currConversationId, inited]) |
|
|
|
|
|
|
|
const handleConversationIdChange = (id: string) => { |
|
|
|
if (id === '-1') { |
|
|
|
createNewChat() |
|
|
|
setConversationIdChangeBecauseOfNew(true) |
|
|
|
} |
|
|
|
else { |
|
|
|
setConversationIdChangeBecauseOfNew(false) |
|
|
|
} |
|
|
|
// trigger handleConversationSwitch |
|
|
|
setCurrConversationId(id, appId) |
|
|
|
setIsShowSuggestion(false) |
|
|
|
hideSidebar() |
|
|
|
} |
|
|
|
|
|
|
|
/* |
|
|
|
* chat info. chat is under conversation. |
|
|
|
*/ |
|
|
|
const [chatList, setChatList, getChatList] = useGetState<IChatItem[]>([]) |
|
|
|
const chatListDomRef = useRef<HTMLDivElement>(null) |
|
|
|
useEffect(() => { |
|
|
|
// scroll to bottom |
|
|
|
if (chatListDomRef.current) |
|
|
|
@@ -319,22 +338,27 @@ const Main: FC<IMainProps> = ({ |
|
|
|
}, [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, |
|
|
|
}) |
|
|
|
})) |
|
|
|
} |
|
|
|
const handleConversationIdChange = useCallback((id: string) => { |
|
|
|
if (id === '-1') { |
|
|
|
createNewChat() |
|
|
|
setConversationIdChangeBecauseOfNew(true) |
|
|
|
} |
|
|
|
else { |
|
|
|
setConversationIdChangeBecauseOfNew(false) |
|
|
|
} |
|
|
|
// trigger handleConversationSwitch |
|
|
|
setCurrConversationId(id, appId) |
|
|
|
setIsShowSuggestion(false) |
|
|
|
hideSidebar() |
|
|
|
}, [ |
|
|
|
appId, |
|
|
|
createNewChat, |
|
|
|
hideSidebar, |
|
|
|
setCurrConversationId, |
|
|
|
setIsShowSuggestion, |
|
|
|
setConversationIdChangeBecauseOfNew, |
|
|
|
]) |
|
|
|
|
|
|
|
// sometime introduction is not applied to state |
|
|
|
const generateNewChatListWithOpenstatement = (introduction?: string, inputs?: Record<string, any> | null) => { |
|
|
|
@@ -446,14 +470,11 @@ const Main: FC<IMainProps> = ({ |
|
|
|
})() |
|
|
|
}, []) |
|
|
|
|
|
|
|
const [isResponsing, { setTrue: setResponsingTrue, setFalse: setResponsingFalse }] = useBoolean(false) |
|
|
|
const [abortController, setAbortController] = useState<AbortController | null>(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<IMainProps> = ({ |
|
|
|
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<string[]>([]) |
|
|
|
const [messageTaskId, setMessageTaskId] = useState('') |
|
|
|
@@ -755,7 +775,7 @@ const Main: FC<IMainProps> = ({ |
|
|
|
}, 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<IMainProps> = ({ |
|
|
|
}) |
|
|
|
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<IMainProps> = ({ |
|
|
|
return ( |
|
|
|
<Sidebar |
|
|
|
list={conversationList} |
|
|
|
onListChanged={(list) => { |
|
|
|
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<IMainProps> = ({ |
|
|
|
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 <AppUnavailable isUnknwonReason={isUnknwonReason} /> |
|
|
|
|
|
|
|
@@ -824,7 +856,7 @@ const Main: FC<IMainProps> = ({ |
|
|
|
icon_background={siteInfo.icon_background} |
|
|
|
isMobile={isMobile} |
|
|
|
onShowSideBar={showSidebar} |
|
|
|
onCreateNewChat={() => handleConversationIdChange('-1')} |
|
|
|
onCreateNewChat={handleStartChatOnSidebar} |
|
|
|
/> |
|
|
|
)} |
|
|
|
|
|
|
|
@@ -884,11 +916,7 @@ const Main: FC<IMainProps> = ({ |
|
|
|
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} |