| } from '@/service/share' | } from '@/service/share' | ||||
| import AppIcon from '@/app/components/base/app-icon' | import AppIcon from '@/app/components/base/app-icon' | ||||
| import AnswerIcon from '@/app/components/base/answer-icon' | import AnswerIcon from '@/app/components/base/answer-icon' | ||||
| import SuggestedQuestions from '@/app/components/base/chat/chat/answer/suggested-questions' | |||||
| import { Markdown } from '@/app/components/base/markdown' | |||||
| import cn from '@/utils/classnames' | import cn from '@/utils/classnames' | ||||
| const ChatWrapper = () => { | const ChatWrapper = () => { | ||||
| currentChatInstanceRef, | currentChatInstanceRef, | ||||
| appData, | appData, | ||||
| themeBuilder, | themeBuilder, | ||||
| sidebarCollapseState, | |||||
| clearChatList, | |||||
| setClearChatList, | |||||
| setIsResponding, | |||||
| } = useChatWithHistoryContext() | } = useChatWithHistoryContext() | ||||
| const appConfig = useMemo(() => { | const appConfig = useMemo(() => { | ||||
| const config = appParams || {} | const config = appParams || {} | ||||
| setTargetMessageId, | setTargetMessageId, | ||||
| handleSend, | handleSend, | ||||
| handleStop, | handleStop, | ||||
| isResponding, | |||||
| isResponding: respondingState, | |||||
| suggestedQuestions, | suggestedQuestions, | ||||
| } = useChat( | } = useChat( | ||||
| appConfig, | appConfig, | ||||
| }, | }, | ||||
| appPrevChatTree, | appPrevChatTree, | ||||
| taskId => stopChatMessageResponding('', taskId, isInstalledApp, appId), | taskId => stopChatMessageResponding('', taskId, isInstalledApp, appId), | ||||
| clearChatList, | |||||
| setClearChatList, | |||||
| ) | ) | ||||
| const inputsFormValue = currentConversationId ? currentConversationItem?.inputs : newConversationInputsRef?.current | const inputsFormValue = currentConversationId ? currentConversationItem?.inputs : newConversationInputsRef?.current | ||||
| const inputDisabled = useMemo(() => { | const inputDisabled = useMemo(() => { | ||||
| // eslint-disable-next-line react-hooks/exhaustive-deps | // eslint-disable-next-line react-hooks/exhaustive-deps | ||||
| }, []) | }, []) | ||||
| useEffect(() => { | |||||
| setIsResponding(respondingState) | |||||
| }, [respondingState, setIsResponding]) | |||||
| const doSend: OnSend = useCallback((message, files, isRegenerate = false, parentAnswer: ChatItem | null = null) => { | const doSend: OnSend = useCallback((message, files, isRegenerate = false, parentAnswer: ChatItem | null = null) => { | ||||
| const data: any = { | const data: any = { | ||||
| query: message, | query: message, | ||||
| const welcome = useMemo(() => { | const welcome = useMemo(() => { | ||||
| const welcomeMessage = chatList.find(item => item.isOpeningStatement) | const welcomeMessage = chatList.find(item => item.isOpeningStatement) | ||||
| if (respondingState) | |||||
| return null | |||||
| if (currentConversationId) | if (currentConversationId) | ||||
| return null | return null | ||||
| if (!welcomeMessage) | if (!welcomeMessage) | ||||
| return null | return null | ||||
| if (!collapsed && inputsForms.length > 0) | if (!collapsed && inputsForms.length > 0) | ||||
| return null | return null | ||||
| if (welcomeMessage.suggestedQuestions && welcomeMessage.suggestedQuestions?.length > 0) { | |||||
| return ( | |||||
| <div className='h-[50vh] py-12 px-4 flex items-center justify-center'> | |||||
| <div className='grow max-w-[720px] flex gap-4'> | |||||
| <AppIcon | |||||
| size='xl' | |||||
| iconType={appData?.site.icon_type} | |||||
| icon={appData?.site.icon} | |||||
| background={appData?.site.icon_background} | |||||
| imageUrl={appData?.site.icon_url} | |||||
| /> | |||||
| <div className='grow px-4 py-3 bg-chat-bubble-bg text-text-primary rounded-2xl body-lg-regular'> | |||||
| <Markdown content={welcomeMessage.content} /> | |||||
| <SuggestedQuestions item={welcomeMessage} /> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| ) | |||||
| } | |||||
| return ( | return ( | ||||
| <div className={cn('h-[50vh] py-12 flex flex-col items-center justify-center gap-3')}> | <div className={cn('h-[50vh] py-12 flex flex-col items-center justify-center gap-3')}> | ||||
| <AppIcon | <AppIcon | ||||
| background={appData?.site.icon_background} | background={appData?.site.icon_background} | ||||
| imageUrl={appData?.site.icon_url} | imageUrl={appData?.site.icon_url} | ||||
| /> | /> | ||||
| <div className='text-text-tertiary body-2xl-regular'>{welcomeMessage.content}</div> | |||||
| <Markdown className='!text-text-tertiary !body-2xl-regular' content={welcomeMessage.content} /> | |||||
| </div> | </div> | ||||
| ) | ) | ||||
| }, [appData?.site.icon, appData?.site.icon_background, appData?.site.icon_type, appData?.site.icon_url, chatList, collapsed, currentConversationId, inputsForms.length]) | |||||
| }, [appData?.site.icon, appData?.site.icon_background, appData?.site.icon_type, appData?.site.icon_url, chatList, collapsed, currentConversationId, inputsForms.length, respondingState]) | |||||
| const answerIcon = (appData?.site && appData.site.use_icon_as_answer_icon) | const answerIcon = (appData?.site && appData.site.use_icon_as_answer_icon) | ||||
| ? <AnswerIcon | ? <AnswerIcon | ||||
| appData={appData} | appData={appData} | ||||
| config={appConfig} | config={appConfig} | ||||
| chatList={messageList} | chatList={messageList} | ||||
| isResponding={isResponding} | |||||
| chatContainerInnerClassName={`mx-auto pt-6 w-full max-w-[720px] ${isMobile && 'px-4'}`} | |||||
| isResponding={respondingState} | |||||
| chatContainerInnerClassName={`mx-auto pt-6 w-full max-w-[768px] ${isMobile && 'px-4'}`} | |||||
| chatFooterClassName='pb-4' | chatFooterClassName='pb-4' | ||||
| chatFooterInnerClassName={`mx-auto w-full max-w-[720px] ${isMobile ? 'px-2' : 'px-4'}`} | |||||
| chatFooterInnerClassName={`mx-auto w-full max-w-[768px] ${isMobile ? 'px-2' : 'px-4'}`} | |||||
| onSend={doSend} | onSend={doSend} | ||||
| inputs={currentConversationId ? currentConversationItem?.inputs as any : newConversationInputs} | inputs={currentConversationId ? currentConversationItem?.inputs as any : newConversationInputs} | ||||
| inputsForm={inputsForms} | inputsForm={inputsForms} | ||||
| switchSibling={siblingMessageId => setTargetMessageId(siblingMessageId)} | switchSibling={siblingMessageId => setTargetMessageId(siblingMessageId)} | ||||
| inputDisabled={inputDisabled} | inputDisabled={inputDisabled} | ||||
| isMobile={isMobile} | isMobile={isMobile} | ||||
| sidebarCollapseState={sidebarCollapseState} | |||||
| /> | /> | ||||
| </div> | </div> | ||||
| ) | ) |
| themeBuilder?: ThemeBuilder | themeBuilder?: ThemeBuilder | ||||
| sidebarCollapseState?: boolean | sidebarCollapseState?: boolean | ||||
| handleSidebarCollapse: (state: boolean) => void | handleSidebarCollapse: (state: boolean) => void | ||||
| clearChatList?: boolean | |||||
| setClearChatList: (state: boolean) => void | |||||
| isResponding?: boolean | |||||
| setIsResponding: (state: boolean) => void, | |||||
| } | } | ||||
| export const ChatWithHistoryContext = createContext<ChatWithHistoryContextValue>({ | export const ChatWithHistoryContext = createContext<ChatWithHistoryContextValue>({ | ||||
| currentChatInstanceRef: { current: { handleStop: () => {} } }, | currentChatInstanceRef: { current: { handleStop: () => {} } }, | ||||
| sidebarCollapseState: false, | sidebarCollapseState: false, | ||||
| handleSidebarCollapse: () => {}, | handleSidebarCollapse: () => {}, | ||||
| clearChatList: false, | |||||
| setClearChatList: () => {}, | |||||
| isResponding: false, | |||||
| setIsResponding: () => {}, | |||||
| }) | }) | ||||
| export const useChatWithHistoryContext = () => useContext(ChatWithHistoryContext) | export const useChatWithHistoryContext = () => useContext(ChatWithHistoryContext) |
| useChatWithHistoryContext, | useChatWithHistoryContext, | ||||
| } from '../context' | } from '../context' | ||||
| import Operation from './operation' | import Operation from './operation' | ||||
| import ActionButton from '@/app/components/base/action-button' | |||||
| import ActionButton, { ActionButtonState } from '@/app/components/base/action-button' | |||||
| import AppIcon from '@/app/components/base/app-icon' | import AppIcon from '@/app/components/base/app-icon' | ||||
| import Tooltip from '@/app/components/base/tooltip' | import Tooltip from '@/app/components/base/tooltip' | ||||
| import ViewFormDropdown from '@/app/components/base/chat/chat-with-history/inputs-form/view-form-dropdown' | import ViewFormDropdown from '@/app/components/base/chat/chat-with-history/inputs-form/view-form-dropdown' | ||||
| handleNewConversation, | handleNewConversation, | ||||
| sidebarCollapseState, | sidebarCollapseState, | ||||
| handleSidebarCollapse, | handleSidebarCollapse, | ||||
| isResponding, | |||||
| } = useChatWithHistoryContext() | } = useChatWithHistoryContext() | ||||
| const { t } = useTranslation() | const { t } = useTranslation() | ||||
| const isSidebarCollapsed = sidebarCollapseState | const isSidebarCollapsed = sidebarCollapseState | ||||
| <div className='h-[14px] w-px bg-divider-regular'></div> | <div className='h-[14px] w-px bg-divider-regular'></div> | ||||
| </div> | </div> | ||||
| {isSidebarCollapsed && ( | {isSidebarCollapsed && ( | ||||
| <ActionButton size='l' onClick={handleNewConversation}> | |||||
| <RiEditBoxLine className='w-[18px] h-[18px]' /> | |||||
| </ActionButton> | |||||
| <Tooltip | |||||
| disabled={!!currentConversationId} | |||||
| popupContent={t('share.chat.newChatTip')} | |||||
| > | |||||
| <div> | |||||
| <ActionButton | |||||
| size='l' | |||||
| state={(!currentConversationId || isResponding) ? ActionButtonState.Disabled : ActionButtonState.Default} | |||||
| disabled={!currentConversationId || isResponding} | |||||
| onClick={handleNewConversation} | |||||
| > | |||||
| <RiEditBoxLine className='w-[18px] h-[18px]' /> | |||||
| </ActionButton> | |||||
| </div> | |||||
| </Tooltip> | |||||
| )} | )} | ||||
| </div> | </div> | ||||
| <div className='flex items-center gap-1'> | <div className='flex items-center gap-1'> |
| const { data: appConversationData, isLoading: appConversationDataLoading, mutate: mutateAppConversationData } = useSWR(['appConversationData', isInstalledApp, appId, false], () => fetchConversations(isInstalledApp, appId, undefined, false, 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 } = useSWR(chatShouldReloadKey ? ['appChatList', chatShouldReloadKey, isInstalledApp, appId] : null, () => fetchChatList(chatShouldReloadKey, isInstalledApp, appId)) | ||||
| const [clearChatList, setClearChatList] = useState(false) | |||||
| const [isResponding, setIsResponding] = useState(false) | |||||
| const appPrevChatTree = useMemo( | const appPrevChatTree = useMemo( | ||||
| () => (currentConversationId && appChatListData?.data.length) | () => (currentConversationId && appChatListData?.data.length) | ||||
| ? buildChatItemTree(getFormattedChatList(appChatListData.data)) | ? buildChatItemTree(getFormattedChatList(appChatListData.data)) | ||||
| currentChatInstanceRef.current.handleStop() | currentChatInstanceRef.current.handleStop() | ||||
| setNewConversationId('') | setNewConversationId('') | ||||
| handleConversationIdInfoChange(conversationId) | handleConversationIdInfoChange(conversationId) | ||||
| }, [handleConversationIdInfoChange]) | |||||
| if (conversationId) | |||||
| setClearChatList(false) | |||||
| }, [handleConversationIdInfoChange, setClearChatList]) | |||||
| const handleNewConversation = useCallback(() => { | const handleNewConversation = useCallback(() => { | ||||
| currentChatInstanceRef.current.handleStop() | currentChatInstanceRef.current.handleStop() | ||||
| setNewConversationId('') | |||||
| if (showNewConversationItemInList) { | |||||
| handleChangeConversation('') | |||||
| } | |||||
| else if (currentConversationId) { | |||||
| handleConversationIdInfoChange('') | |||||
| setShowNewConversationItemInList(true) | |||||
| handleNewConversationInputsChange({}) | |||||
| } | |||||
| }, [handleChangeConversation, currentConversationId, handleConversationIdInfoChange, setShowNewConversationItemInList, showNewConversationItemInList, handleNewConversationInputsChange]) | |||||
| setShowNewConversationItemInList(true) | |||||
| handleChangeConversation('') | |||||
| handleNewConversationInputsChange({}) | |||||
| setClearChatList(true) | |||||
| }, [handleChangeConversation, setShowNewConversationItemInList, handleNewConversationInputsChange, setClearChatList]) | |||||
| const handleUpdateConversationList = useCallback(() => { | const handleUpdateConversationList = useCallback(() => { | ||||
| mutateAppConversationData() | mutateAppConversationData() | ||||
| mutateAppPinnedConversationData() | mutateAppPinnedConversationData() | ||||
| currentChatInstanceRef, | currentChatInstanceRef, | ||||
| sidebarCollapseState, | sidebarCollapseState, | ||||
| handleSidebarCollapse, | handleSidebarCollapse, | ||||
| clearChatList, | |||||
| setClearChatList, | |||||
| isResponding, | |||||
| setIsResponding, | |||||
| } | } | ||||
| } | } |
| {isMobile && ( | {isMobile && ( | ||||
| <HeaderInMobile /> | <HeaderInMobile /> | ||||
| )} | )} | ||||
| <div className={cn('relative grow p-2')}> | |||||
| <div className={cn('relative grow p-2', isMobile && 'h-[calc(100%_-_56px)] p-0')}> | |||||
| {isSidebarCollapsed && ( | {isSidebarCollapsed && ( | ||||
| <div | <div | ||||
| className={cn( | className={cn( | ||||
| <Sidebar isPanel /> | <Sidebar isPanel /> | ||||
| </div> | </div> | ||||
| )} | )} | ||||
| <div className='h-full flex flex-col bg-chatbot-bg rounded-2xl border-[0,5px] border-components-panel-border-subtle overflow-hidden'> | |||||
| <div className={cn('h-full flex flex-col bg-chatbot-bg border-[0,5px] border-components-panel-border-subtle overflow-hidden', isMobile ? 'rounded-t-2xl' : 'rounded-2xl')}> | |||||
| {!isMobile && <Header />} | {!isMobile && <Header />} | ||||
| {appChatListDataLoading && ( | {appChatListDataLoading && ( | ||||
| <Loading type='app' /> | <Loading type='app' /> | ||||
| currentChatInstanceRef, | currentChatInstanceRef, | ||||
| sidebarCollapseState, | sidebarCollapseState, | ||||
| handleSidebarCollapse, | handleSidebarCollapse, | ||||
| clearChatList, | |||||
| setClearChatList, | |||||
| isResponding, | |||||
| setIsResponding, | |||||
| } = useChatWithHistory(installedAppInfo) | } = useChatWithHistory(installedAppInfo) | ||||
| return ( | return ( | ||||
| themeBuilder, | themeBuilder, | ||||
| sidebarCollapseState, | sidebarCollapseState, | ||||
| handleSidebarCollapse, | handleSidebarCollapse, | ||||
| clearChatList, | |||||
| setClearChatList, | |||||
| isResponding, | |||||
| setIsResponding, | |||||
| }}> | }}> | ||||
| <ChatWithHistory className={className} /> | <ChatWithHistory className={className} /> | ||||
| </ChatWithHistoryContext.Provider> | </ChatWithHistoryContext.Provider> |
| sidebarCollapseState, | sidebarCollapseState, | ||||
| handleSidebarCollapse, | handleSidebarCollapse, | ||||
| isMobile, | isMobile, | ||||
| isResponding, | |||||
| } = useChatWithHistoryContext() | } = useChatWithHistoryContext() | ||||
| const isSidebarCollapsed = sidebarCollapseState | const isSidebarCollapsed = sidebarCollapseState | ||||
| )} | )} | ||||
| </div> | </div> | ||||
| <div className='shrink-0 px-3 py-4'> | <div className='shrink-0 px-3 py-4'> | ||||
| <Button variant='secondary-accent' className='w-full justify-center' onClick={handleNewConversation}> | |||||
| <Button variant='secondary-accent' disabled={isResponding} className='w-full justify-center' onClick={handleNewConversation}> | |||||
| <RiEditBoxLine className='w-4 h-4 mr-1' /> | <RiEditBoxLine className='w-4 h-4 mr-1' /> | ||||
| {t('share.chat.newChat')} | {t('share.chat.newChat')} | ||||
| </Button> | </Button> |
| </div> | </div> | ||||
| )} | )} | ||||
| </div> | </div> | ||||
| <div className='chat-answer-container group grow w-0 ml-4' ref={containerRef}> | |||||
| <div className='chat-answer-container group grow w-0 ml-4 pb-4' ref={containerRef}> | |||||
| <div className={cn('group relative pr-10', chatAnswerContainerInner)}> | <div className={cn('group relative pr-10', chatAnswerContainerInner)}> | ||||
| <div | <div | ||||
| ref={contentRef} | ref={contentRef} |
| import { | import { | ||||
| RiClipboardLine, | RiClipboardLine, | ||||
| RiEditLine, | RiEditLine, | ||||
| RiReplay15Line, | |||||
| RiResetLeftLine, | |||||
| RiThumbDownLine, | RiThumbDownLine, | ||||
| RiThumbUpLine, | RiThumbUpLine, | ||||
| } from '@remixicon/react' | } from '@remixicon/react' | ||||
| </ActionButton> | </ActionButton> | ||||
| {!noChatInput && ( | {!noChatInput && ( | ||||
| <ActionButton onClick={() => onRegenerate?.(item)}> | <ActionButton onClick={() => onRegenerate?.(item)}> | ||||
| <RiReplay15Line className='w-4 h-4' /> | |||||
| <RiResetLeftLine className='w-4 h-4' /> | |||||
| </ActionButton> | </ActionButton> | ||||
| )} | )} | ||||
| {(config?.supportAnnotation && config.annotation_reply?.enabled) && ( | {(config?.supportAnnotation && config.annotation_reply?.enabled) && ( |
| }, | }, | ||||
| prevChatTree?: ChatItemInTree[], | prevChatTree?: ChatItemInTree[], | ||||
| stopChat?: (taskId: string) => void, | stopChat?: (taskId: string) => void, | ||||
| clearChatList?: boolean, | |||||
| clearChatListCallback?: (state: boolean) => void, | |||||
| ) => { | ) => { | ||||
| const { t } = useTranslation() | const { t } = useTranslation() | ||||
| const { formatTime } = useTimestamp() | const { formatTime } = useTimestamp() | ||||
| } | } | ||||
| else { | else { | ||||
| ret.unshift({ | ret.unshift({ | ||||
| id: `${Date.now()}`, | |||||
| id: 'opening-statement', | |||||
| content: getIntroduction(config.opening_statement), | content: getIntroduction(config.opening_statement), | ||||
| isAnswer: true, | isAnswer: true, | ||||
| isOpeningStatement: true, | isOpeningStatement: true, | ||||
| suggestedQuestionsAbortControllerRef.current.abort() | suggestedQuestionsAbortControllerRef.current.abort() | ||||
| }, [stopChat, handleResponding]) | }, [stopChat, handleResponding]) | ||||
| const handleRestart = useCallback(() => { | |||||
| const handleRestart = useCallback((cb?: any) => { | |||||
| conversationId.current = '' | conversationId.current = '' | ||||
| taskIdRef.current = '' | taskIdRef.current = '' | ||||
| handleStop() | handleStop() | ||||
| setChatTree([]) | setChatTree([]) | ||||
| setSuggestQuestions([]) | setSuggestQuestions([]) | ||||
| cb?.() | |||||
| }, [handleStop]) | }, [handleStop]) | ||||
| const updateCurrentQAOnTree = useCallback(({ | const updateCurrentQAOnTree = useCallback(({ | ||||
| }) | }) | ||||
| }, [chatList, updateChatTreeNode]) | }, [chatList, updateChatTreeNode]) | ||||
| useEffect(() => { | |||||
| if (clearChatList) | |||||
| handleRestart(() => clearChatListCallback?.(false)) | |||||
| }, [clearChatList, clearChatListCallback, handleRestart]) | |||||
| return { | return { | ||||
| chatList, | chatList, | ||||
| setTargetMessageId, | setTargetMessageId, |
| noSpacing?: boolean | noSpacing?: boolean | ||||
| inputDisabled?: boolean | inputDisabled?: boolean | ||||
| isMobile?: boolean | isMobile?: boolean | ||||
| sidebarCollapseState?: boolean | |||||
| } | } | ||||
| const Chat: FC<ChatProps> = ({ | const Chat: FC<ChatProps> = ({ | ||||
| noSpacing, | noSpacing, | ||||
| inputDisabled, | inputDisabled, | ||||
| isMobile, | isMobile, | ||||
| sidebarCollapseState, | |||||
| }) => { | }) => { | ||||
| const { t } = useTranslation() | const { t } = useTranslation() | ||||
| const { currentLogItem, setCurrentLogItem, showPromptLogModal, setShowPromptLogModal, showAgentLogModal, setShowAgentLogModal } = useAppStore(useShallow(state => ({ | const { currentLogItem, setCurrentLogItem, showPromptLogModal, setShowPromptLogModal, showAgentLogModal, setShowAgentLogModal } = useAppStore(useShallow(state => ({ | ||||
| } | } | ||||
| }, []) | }, []) | ||||
| useEffect(() => { | |||||
| if (!sidebarCollapseState) | |||||
| setTimeout(() => handleWindowResize(), 200) | |||||
| }, [sidebarCollapseState]) | |||||
| const hasTryToAsk = config?.suggested_questions_after_answer?.enabled && !!suggestedQuestions?.length && onSend | const hasTryToAsk = config?.suggested_questions_after_answer?.enabled && !!suggestedQuestions?.length && onSend | ||||
| return ( | return ( | ||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| <div | <div | ||||
| className={`absolute bottom-0 bg-chat-input-mask ${(hasTryToAsk || !noChatInput || !noStopResponding) && chatFooterClassName}`} | |||||
| className={`absolute bottom-0 bg-chat-input-mask flex justify-center ${(hasTryToAsk || !noChatInput || !noStopResponding) && chatFooterClassName}`} | |||||
| ref={chatFooterRef} | ref={chatFooterRef} | ||||
| > | > | ||||
| <div | <div |
| import AppIcon from '@/app/components/base/app-icon' | import AppIcon from '@/app/components/base/app-icon' | ||||
| import LogoAvatar from '@/app/components/base/logo/logo-embedded-chat-avatar' | import LogoAvatar from '@/app/components/base/logo/logo-embedded-chat-avatar' | ||||
| import AnswerIcon from '@/app/components/base/answer-icon' | import AnswerIcon from '@/app/components/base/answer-icon' | ||||
| import SuggestedQuestions from '@/app/components/base/chat/chat/answer/suggested-questions' | |||||
| import { Markdown } from '@/app/components/base/markdown' | |||||
| import cn from '@/utils/classnames' | import cn from '@/utils/classnames' | ||||
| const ChatWrapper = () => { | const ChatWrapper = () => { | ||||
| handleFeedback, | handleFeedback, | ||||
| currentChatInstanceRef, | currentChatInstanceRef, | ||||
| themeBuilder, | themeBuilder, | ||||
| clearChatList, | |||||
| setClearChatList, | |||||
| setIsResponding, | |||||
| } = useEmbeddedChatbotContext() | } = useEmbeddedChatbotContext() | ||||
| const appConfig = useMemo(() => { | const appConfig = useMemo(() => { | ||||
| const config = appParams || {} | const config = appParams || {} | ||||
| setTargetMessageId, | setTargetMessageId, | ||||
| handleSend, | handleSend, | ||||
| handleStop, | handleStop, | ||||
| isResponding, | |||||
| isResponding: respondingState, | |||||
| suggestedQuestions, | suggestedQuestions, | ||||
| } = useChat( | } = useChat( | ||||
| appConfig, | appConfig, | ||||
| }, | }, | ||||
| appPrevChatList, | appPrevChatList, | ||||
| taskId => stopChatMessageResponding('', taskId, isInstalledApp, appId), | taskId => stopChatMessageResponding('', taskId, isInstalledApp, appId), | ||||
| clearChatList, | |||||
| setClearChatList, | |||||
| ) | ) | ||||
| const inputsFormValue = currentConversationId ? currentConversationItem?.inputs : newConversationInputsRef?.current | const inputsFormValue = currentConversationId ? currentConversationItem?.inputs : newConversationInputsRef?.current | ||||
| const inputDisabled = useMemo(() => { | const inputDisabled = useMemo(() => { | ||||
| if (currentChatInstanceRef.current) | if (currentChatInstanceRef.current) | ||||
| currentChatInstanceRef.current.handleStop = handleStop | currentChatInstanceRef.current.handleStop = handleStop | ||||
| }, [currentChatInstanceRef, handleStop]) | }, [currentChatInstanceRef, handleStop]) | ||||
| useEffect(() => { | |||||
| setIsResponding(respondingState) | |||||
| }, [respondingState, setIsResponding]) | |||||
| const doSend: OnSend = useCallback((message, files, isRegenerate = false, parentAnswer: ChatItem | null = null) => { | const doSend: OnSend = useCallback((message, files, isRegenerate = false, parentAnswer: ChatItem | null = null) => { | ||||
| const data: any = { | const data: any = { | ||||
| const welcome = useMemo(() => { | const welcome = useMemo(() => { | ||||
| const welcomeMessage = chatList.find(item => item.isOpeningStatement) | const welcomeMessage = chatList.find(item => item.isOpeningStatement) | ||||
| if (respondingState) | |||||
| return null | |||||
| if (currentConversationId) | if (currentConversationId) | ||||
| return null | return null | ||||
| if (!welcomeMessage) | if (!welcomeMessage) | ||||
| return null | return null | ||||
| if (!collapsed && inputsForms.length > 0) | if (!collapsed && inputsForms.length > 0) | ||||
| return null | return null | ||||
| if (welcomeMessage.suggestedQuestions && welcomeMessage.suggestedQuestions?.length > 0) { | |||||
| return ( | |||||
| <div className='h-[50vh] py-12 px-4 flex items-center justify-center'> | |||||
| <div className='grow max-w-[720px] flex gap-4'> | |||||
| <AppIcon | |||||
| size='xl' | |||||
| iconType={appData?.site.icon_type} | |||||
| icon={appData?.site.icon} | |||||
| background={appData?.site.icon_background} | |||||
| imageUrl={appData?.site.icon_url} | |||||
| /> | |||||
| <div className='grow px-4 py-3 bg-chat-bubble-bg text-text-primary rounded-2xl body-lg-regular'> | |||||
| <Markdown content={welcomeMessage.content} /> | |||||
| <SuggestedQuestions item={welcomeMessage} /> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| ) | |||||
| } | |||||
| return ( | return ( | ||||
| <div className={cn('h-[50vh] py-12 flex flex-col items-center justify-center gap-3')}> | <div className={cn('h-[50vh] py-12 flex flex-col items-center justify-center gap-3')}> | ||||
| <AppIcon | <AppIcon | ||||
| background={appData?.site.icon_background} | background={appData?.site.icon_background} | ||||
| imageUrl={appData?.site.icon_url} | imageUrl={appData?.site.icon_url} | ||||
| /> | /> | ||||
| <div className='text-text-tertiary body-2xl-regular'>{welcomeMessage.content}</div> | |||||
| <Markdown className='!text-text-tertiary !body-2xl-regular' content={welcomeMessage.content} /> | |||||
| </div> | </div> | ||||
| ) | ) | ||||
| }, [appData?.site.icon, appData?.site.icon_background, appData?.site.icon_type, appData?.site.icon_url, chatList, collapsed, currentConversationId, inputsForms.length]) | |||||
| }, [appData?.site.icon, appData?.site.icon_background, appData?.site.icon_type, appData?.site.icon_url, chatList, collapsed, currentConversationId, inputsForms.length, respondingState]) | |||||
| const answerIcon = isDify() | const answerIcon = isDify() | ||||
| ? <LogoAvatar className='relative shrink-0' /> | ? <LogoAvatar className='relative shrink-0' /> | ||||
| appData={appData} | appData={appData} | ||||
| config={appConfig} | config={appConfig} | ||||
| chatList={messageList} | chatList={messageList} | ||||
| isResponding={isResponding} | |||||
| chatContainerInnerClassName={cn('mx-auto w-full max-w-full tablet:px-4', isMobile && 'px-4')} | |||||
| isResponding={respondingState} | |||||
| chatContainerInnerClassName={cn('mx-auto w-full max-w-full pt-4 tablet:px-4', isMobile && 'px-4')} | |||||
| chatFooterClassName={cn('pb-4', !isMobile && 'rounded-b-2xl')} | chatFooterClassName={cn('pb-4', !isMobile && 'rounded-b-2xl')} | ||||
| chatFooterInnerClassName={cn('mx-auto w-full max-w-full tablet:px-4', isMobile && 'px-2')} | |||||
| chatFooterInnerClassName={cn('mx-auto w-full max-w-full px-4', isMobile && 'px-2')} | |||||
| onSend={doSend} | onSend={doSend} | ||||
| inputs={currentConversationId ? currentConversationItem?.inputs as any : newConversationInputs} | inputs={currentConversationId ? currentConversationItem?.inputs as any : newConversationInputs} | ||||
| inputsForm={inputsForms} | inputsForm={inputsForms} |
| handleFeedback: (messageId: string, feedback: Feedback) => void | handleFeedback: (messageId: string, feedback: Feedback) => void | ||||
| currentChatInstanceRef: RefObject<{ handleStop: () => void }> | currentChatInstanceRef: RefObject<{ handleStop: () => void }> | ||||
| themeBuilder?: ThemeBuilder | themeBuilder?: ThemeBuilder | ||||
| clearChatList?: boolean | |||||
| setClearChatList: (state: boolean) => void | |||||
| isResponding?: boolean | |||||
| setIsResponding: (state: boolean) => void, | |||||
| } | } | ||||
| export const EmbeddedChatbotContext = createContext<EmbeddedChatbotContextValue>({ | export const EmbeddedChatbotContext = createContext<EmbeddedChatbotContextValue>({ | ||||
| isInstalledApp: false, | isInstalledApp: false, | ||||
| handleFeedback: () => {}, | handleFeedback: () => {}, | ||||
| currentChatInstanceRef: { current: { handleStop: () => {} } }, | currentChatInstanceRef: { current: { handleStop: () => {} } }, | ||||
| clearChatList: false, | |||||
| setClearChatList: () => {}, | |||||
| isResponding: false, | |||||
| setIsResponding: () => {}, | |||||
| }) | }) | ||||
| export const useEmbeddedChatbotContext = () => useContext(EmbeddedChatbotContext) | export const useEmbeddedChatbotContext = () => useContext(EmbeddedChatbotContext) |
| const { data: appConversationData, isLoading: appConversationDataLoading, mutate: mutateAppConversationData } = useSWR(['appConversationData', isInstalledApp, appId, false], () => fetchConversations(isInstalledApp, appId, undefined, false, 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 } = useSWR(chatShouldReloadKey ? ['appChatList', chatShouldReloadKey, isInstalledApp, appId] : null, () => fetchChatList(chatShouldReloadKey, isInstalledApp, appId)) | ||||
| const [clearChatList, setClearChatList] = useState(false) | |||||
| const [isResponding, setIsResponding] = useState(false) | |||||
| const appPrevChatList = useMemo( | const appPrevChatList = useMemo( | ||||
| () => (currentConversationId && appChatListData?.data.length) | () => (currentConversationId && appChatListData?.data.length) | ||||
| ? buildChatItemTree(getFormattedChatList(appChatListData.data)) | ? buildChatItemTree(getFormattedChatList(appChatListData.data)) | ||||
| currentChatInstanceRef.current.handleStop() | currentChatInstanceRef.current.handleStop() | ||||
| setNewConversationId('') | setNewConversationId('') | ||||
| handleConversationIdInfoChange(conversationId) | handleConversationIdInfoChange(conversationId) | ||||
| }, [handleConversationIdInfoChange]) | |||||
| if (conversationId) | |||||
| setClearChatList(false) | |||||
| }, [handleConversationIdInfoChange, setClearChatList]) | |||||
| const handleNewConversation = useCallback(() => { | const handleNewConversation = useCallback(() => { | ||||
| currentChatInstanceRef.current.handleStop() | currentChatInstanceRef.current.handleStop() | ||||
| setNewConversationId('') | |||||
| if (showNewConversationItemInList) { | |||||
| handleChangeConversation('') | |||||
| } | |||||
| else if (currentConversationId) { | |||||
| handleConversationIdInfoChange('') | |||||
| setShowNewConversationItemInList(true) | |||||
| handleNewConversationInputsChange({}) | |||||
| } | |||||
| }, [handleChangeConversation, currentConversationId, handleConversationIdInfoChange, setShowNewConversationItemInList, showNewConversationItemInList, handleNewConversationInputsChange]) | |||||
| setShowNewConversationItemInList(true) | |||||
| handleChangeConversation('') | |||||
| handleNewConversationInputsChange({}) | |||||
| setClearChatList(true) | |||||
| }, [handleChangeConversation, setShowNewConversationItemInList, handleNewConversationInputsChange, setClearChatList]) | |||||
| const handleNewConversationCompleted = useCallback((newConversationId: string) => { | const handleNewConversationCompleted = useCallback((newConversationId: string) => { | ||||
| setNewConversationId(newConversationId) | setNewConversationId(newConversationId) | ||||
| chatShouldReloadKey, | chatShouldReloadKey, | ||||
| handleFeedback, | handleFeedback, | ||||
| currentChatInstanceRef, | currentChatInstanceRef, | ||||
| clearChatList, | |||||
| setClearChatList, | |||||
| isResponding, | |||||
| setIsResponding, | |||||
| } | } | ||||
| } | } |
| appId, | appId, | ||||
| handleFeedback, | handleFeedback, | ||||
| currentChatInstanceRef, | currentChatInstanceRef, | ||||
| clearChatList, | |||||
| setClearChatList, | |||||
| isResponding, | |||||
| setIsResponding, | |||||
| } = useEmbeddedChatbot() | } = useEmbeddedChatbot() | ||||
| return <EmbeddedChatbotContext.Provider value={{ | return <EmbeddedChatbotContext.Provider value={{ | ||||
| handleFeedback, | handleFeedback, | ||||
| currentChatInstanceRef, | currentChatInstanceRef, | ||||
| themeBuilder, | themeBuilder, | ||||
| clearChatList, | |||||
| setClearChatList, | |||||
| isResponding, | |||||
| setIsResponding, | |||||
| }}> | }}> | ||||
| <Chatbot /> | <Chatbot /> | ||||
| </EmbeddedChatbotContext.Provider> | </EmbeddedChatbotContext.Provider> |
| display: block; | display: block; | ||||
| width: max-content; | width: max-content; | ||||
| max-width: 100%; | max-width: 100%; | ||||
| overflow: hidden; | |||||
| overflow: auto; | |||||
| border: 1px solid var(--color-divider-regular); | border: 1px solid var(--color-divider-regular); | ||||
| border-radius: 8px; | border-radius: 8px; | ||||
| } | } |
| }, | }, | ||||
| chat: { | chat: { | ||||
| newChat: 'Start New chat', | newChat: 'Start New chat', | ||||
| newChatTip: 'Already in a new chat', | |||||
| chatSettingsTitle: 'New chat setup', | chatSettingsTitle: 'New chat setup', | ||||
| chatFormTip: 'Chat settings cannot be modified after the chat has started.', | chatFormTip: 'Chat settings cannot be modified after the chat has started.', | ||||
| pinnedTitle: 'Pinned', | pinnedTitle: 'Pinned', |
| }, | }, | ||||
| chat: { | chat: { | ||||
| newChat: '开启新对话', | newChat: '开启新对话', | ||||
| newChatTip: '已在新对话中', | |||||
| chatSettingsTitle: '新对话设置', | chatSettingsTitle: '新对话设置', | ||||
| chatFormTip: '对话开始后,对话设置将无法修改。', | chatFormTip: '对话开始后,对话设置将无法修改。', | ||||
| pinnedTitle: '已置顶', | pinnedTitle: '已置顶', |