123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270 |
- import { useCallback, useEffect, useMemo, useState } from 'react'
- import Chat from '../chat'
- import type {
- ChatConfig,
- ChatItem,
- ChatItemInTree,
- OnSend,
- } from '../types'
- import { useChat } from '../chat/hooks'
- import { getLastAnswer, isValidGeneratedAnswer } from '../utils'
- import { useChatWithHistoryContext } from './context'
- import { InputVarType } from '@/app/components/workflow/types'
- import { TransferMethod } from '@/types/app'
- import InputsForm from '@/app/components/base/chat/chat-with-history/inputs-form'
- import {
- fetchSuggestedQuestions,
- getUrl,
- stopChatMessageResponding,
- } from '@/service/share'
- import AppIcon from '@/app/components/base/app-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'
- const ChatWrapper = () => {
- const {
- appParams,
- appPrevChatTree,
- currentConversationId,
- currentConversationItem,
- inputsForms,
- newConversationInputs,
- newConversationInputsRef,
- handleNewConversationCompleted,
- isMobile,
- isInstalledApp,
- appId,
- appMeta,
- handleFeedback,
- currentChatInstanceRef,
- appData,
- themeBuilder,
- sidebarCollapseState,
- clearChatList,
- setClearChatList,
- setIsResponding,
- } = useChatWithHistoryContext()
- const appConfig = useMemo(() => {
- const config = appParams || {}
- return {
- ...config,
- file_upload: {
- ...(config as any).file_upload,
- fileUploadConfig: (config as any).system_parameters,
- },
- supportFeedback: true,
- opening_statement: currentConversationId ? currentConversationItem?.introduction : (config as any).opening_statement,
- } as ChatConfig
- }, [appParams, currentConversationItem?.introduction, currentConversationId])
- const {
- chatList,
- setTargetMessageId,
- handleSend,
- handleStop,
- isResponding: respondingState,
- suggestedQuestions,
- } = useChat(
- appConfig,
- {
- inputs: (currentConversationId ? currentConversationItem?.inputs : newConversationInputs) as any,
- inputsForm: inputsForms,
- },
- appPrevChatTree,
- taskId => stopChatMessageResponding('', taskId, isInstalledApp, appId),
- clearChatList,
- setClearChatList,
- )
- const inputsFormValue = currentConversationId ? currentConversationItem?.inputs : newConversationInputsRef?.current
- const inputDisabled = useMemo(() => {
- let hasEmptyInput = ''
- let fileIsUploading = false
- const requiredVars = inputsForms.filter(({ required }) => required)
- if (requiredVars.length) {
- requiredVars.forEach(({ variable, label, type }) => {
- if (hasEmptyInput)
- return
- if (fileIsUploading)
- return
- if (!inputsFormValue?.[variable])
- hasEmptyInput = label as string
- if ((type === InputVarType.singleFile || type === InputVarType.multiFiles) && inputsFormValue?.[variable]) {
- const files = inputsFormValue[variable]
- if (Array.isArray(files))
- fileIsUploading = files.find(item => item.transferMethod === TransferMethod.local_file && !item.uploadedId)
- else
- fileIsUploading = files.transferMethod === TransferMethod.local_file && !files.uploadedId
- }
- })
- }
- if (hasEmptyInput)
- return true
- if (fileIsUploading)
- return true
- return false
- }, [inputsFormValue, inputsForms])
- useEffect(() => {
- if (currentChatInstanceRef.current)
- currentChatInstanceRef.current.handleStop = handleStop
- // 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 data: any = {
- query: message,
- files,
- inputs: currentConversationId ? currentConversationItem?.inputs : newConversationInputs,
- conversation_id: currentConversationId,
- parent_message_id: (isRegenerate ? parentAnswer?.id : getLastAnswer(chatList)?.id) || null,
- }
- handleSend(
- getUrl('chat-messages', isInstalledApp, appId || ''),
- data,
- {
- onGetSuggestedQuestions: responseItemId => fetchSuggestedQuestions(responseItemId, isInstalledApp, appId),
- onConversationComplete: currentConversationId ? undefined : handleNewConversationCompleted,
- isPublicAPI: !isInstalledApp,
- },
- )
- }, [
- chatList,
- handleNewConversationCompleted,
- handleSend,
- currentConversationId,
- currentConversationItem,
- newConversationInputs,
- isInstalledApp,
- appId,
- ])
- const doRegenerate = useCallback((chatItem: ChatItemInTree) => {
- const question = chatList.find(item => item.id === chatItem.parentMessageId)!
- const parentAnswer = chatList.find(item => item.id === question.parentMessageId)
- doSend(question.content, question.message_files, true, isValidGeneratedAnswer(parentAnswer) ? parentAnswer : null)
- }, [chatList, doSend])
- const messageList = useMemo(() => {
- if (currentConversationId)
- return chatList
- return chatList.filter(item => !item.isOpeningStatement)
- }, [chatList, currentConversationId])
- const [collapsed, setCollapsed] = useState(!!currentConversationId)
- const chatNode = useMemo(() => {
- if (!inputsForms.length)
- return null
- if (isMobile) {
- if (!currentConversationId)
- return <InputsForm collapsed={collapsed} setCollapsed={setCollapsed} />
- return null
- }
- else {
- return <InputsForm collapsed={collapsed} setCollapsed={setCollapsed} />
- }
- }, [inputsForms.length, isMobile, currentConversationId, collapsed])
- const welcome = useMemo(() => {
- const welcomeMessage = chatList.find(item => item.isOpeningStatement)
- if (respondingState)
- return null
- if (currentConversationId)
- return null
- if (!welcomeMessage)
- return null
- if (!collapsed && inputsForms.length > 0)
- 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 (
- <div className={cn('h-[50vh] py-12 flex flex-col items-center justify-center gap-3')}>
- <AppIcon
- size='xl'
- iconType={appData?.site.icon_type}
- icon={appData?.site.icon}
- background={appData?.site.icon_background}
- imageUrl={appData?.site.icon_url}
- />
- <Markdown className='!text-text-tertiary !body-2xl-regular' content={welcomeMessage.content} />
- </div>
- )
- }, [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)
- ? <AnswerIcon
- iconType={appData.site.icon_type}
- icon={appData.site.icon}
- background={appData.site.icon_background}
- imageUrl={appData.site.icon_url}
- />
- : null
- return (
- <div
- className='h-full bg-chatbot-bg overflow-hidden'
- >
- <Chat
- appData={appData}
- config={appConfig}
- chatList={messageList}
- isResponding={respondingState}
- chatContainerInnerClassName={`mx-auto pt-6 w-full max-w-[768px] ${isMobile && 'px-4'}`}
- chatFooterClassName='pb-4'
- chatFooterInnerClassName={`mx-auto w-full max-w-[768px] ${isMobile ? 'px-2' : 'px-4'}`}
- onSend={doSend}
- inputs={currentConversationId ? currentConversationItem?.inputs as any : newConversationInputs}
- inputsForm={inputsForms}
- onRegenerate={doRegenerate}
- onStopResponding={handleStop}
- chatNode={
- <>
- {chatNode}
- {welcome}
- </>
- }
- allToolIcons={appMeta?.tool_icons || {}}
- onFeedback={handleFeedback}
- suggestedQuestions={suggestedQuestions}
- answerIcon={answerIcon}
- hideProcessDetail
- themeBuilder={themeBuilder}
- switchSibling={siblingMessageId => setTargetMessageId(siblingMessageId)}
- inputDisabled={inputDisabled}
- isMobile={isMobile}
- sidebarCollapseState={sidebarCollapseState}
- />
- </div>
- )
- }
- export default ChatWrapper
|