284 lines
9.7 KiB
TypeScript
Raw Normal View History

2025-03-03 14:44:51 +08:00
import { useCallback, useEffect, useMemo, useState } from 'react'
2024-02-04 16:10:46 +08:00
import Chat from '../chat'
import type {
ChatConfig,
ChatItem,
ChatItemInTree,
2024-02-04 16:10:46 +08:00
OnSend,
} from '../types'
import { useChat } from '../chat/hooks'
import { getLastAnswer, isValidGeneratedAnswer } from '../utils'
2024-02-04 16:10:46 +08:00
import { useChatWithHistoryContext } from './context'
2025-03-03 14:44:51 +08:00
import { InputVarType } from '@/app/components/workflow/types'
import { TransferMethod } from '@/types/app'
import InputsForm from '@/app/components/base/chat/chat-with-history/inputs-form'
2024-02-04 16:10:46 +08:00
import {
fetchSuggestedQuestions,
getUrl,
stopChatMessageResponding,
2024-02-04 16:10:46 +08:00
} from '@/service/share'
2025-03-03 14:44:51 +08:00
import AppIcon from '@/app/components/base/app-icon'
2024-10-22 18:24:13 +08:00
import AnswerIcon from '@/app/components/base/answer-icon'
2025-03-13 14:23:41 +08:00
import SuggestedQuestions from '@/app/components/base/chat/chat/answer/suggested-questions'
import { Markdown } from '@/app/components/base/markdown'
2025-03-03 14:44:51 +08:00
import cn from '@/utils/classnames'
2025-04-15 15:37:08 +08:00
import type { FileEntity } from '../../file-uploader/types'
import Avatar from '../../avatar'
2024-02-04 16:10:46 +08:00
const ChatWrapper = () => {
const {
appParams,
appPrevChatTree,
2024-02-04 16:10:46 +08:00
currentConversationId,
currentConversationItem,
currentConversationInputs,
2024-02-04 16:10:46 +08:00
inputsForms,
newConversationInputs,
2025-03-03 14:44:51 +08:00
newConversationInputsRef,
2024-02-04 16:10:46 +08:00
handleNewConversationCompleted,
isMobile,
isInstalledApp,
appId,
appMeta,
handleFeedback,
currentChatInstanceRef,
appData,
2024-08-02 15:08:14 +08:00
themeBuilder,
2025-03-13 14:23:41 +08:00
sidebarCollapseState,
clearChatList,
setClearChatList,
setIsResponding,
allInputsHidden,
initUserVariables,
2024-02-04 16:10:46 +08:00
} = useChatWithHistoryContext()
const appConfig = useMemo(() => {
const config = appParams || {}
return {
...config,
file_upload: {
...(config as any).file_upload,
fileUploadConfig: (config as any).system_parameters,
},
2024-02-04 16:10:46 +08:00
supportFeedback: true,
2024-02-12 22:22:57 +08:00
opening_statement: currentConversationId ? currentConversationItem?.introduction : (config as any).opening_statement,
2024-02-04 16:10:46 +08:00
} as ChatConfig
2024-02-12 22:22:57 +08:00
}, [appParams, currentConversationItem?.introduction, currentConversationId])
2024-02-04 16:10:46 +08:00
const {
chatList,
setTargetMessageId,
2024-02-04 16:10:46 +08:00
handleSend,
handleStop,
2025-03-13 14:23:41 +08:00
isResponding: respondingState,
2024-02-04 16:10:46 +08:00
suggestedQuestions,
} = useChat(
appConfig,
2024-02-12 22:22:57 +08:00
{
inputs: (currentConversationId ? currentConversationInputs : newConversationInputs) as any,
inputsForm: inputsForms,
2024-02-12 22:22:57 +08:00
},
appPrevChatTree,
taskId => stopChatMessageResponding('', taskId, isInstalledApp, appId),
2025-03-13 14:23:41 +08:00
clearChatList,
setClearChatList,
2024-02-04 16:10:46 +08:00
)
const inputsFormValue = currentConversationId ? currentConversationInputs : newConversationInputsRef?.current
2025-03-03 14:44:51 +08:00
const inputDisabled = useMemo(() => {
if (allInputsHidden)
return false
2025-03-03 14:44:51 +08:00
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, allInputsHidden])
2024-02-04 16:10:46 +08:00
useEffect(() => {
if (currentChatInstanceRef.current)
currentChatInstanceRef.current.handleStop = handleStop
}, [])
2025-03-13 14:23:41 +08:00
useEffect(() => {
setIsResponding(respondingState)
}, [respondingState, setIsResponding])
const doSend: OnSend = useCallback((message, files, isRegenerate = false, parentAnswer: ChatItem | null = null) => {
2024-02-04 16:10:46 +08:00
const data: any = {
query: message,
files,
inputs: currentConversationId ? currentConversationInputs : newConversationInputs,
2024-02-04 16:10:46 +08:00
conversation_id: currentConversationId,
parent_message_id: (isRegenerate ? parentAnswer?.id : getLastAnswer(chatList)?.id) || null,
2024-02-04 16:10:46 +08:00
}
handleSend(
getUrl('chat-messages', isInstalledApp, appId || ''),
data,
{
onGetSuggestedQuestions: responseItemId => fetchSuggestedQuestions(responseItemId, isInstalledApp, appId),
onConversationComplete: currentConversationId ? undefined : handleNewConversationCompleted,
isPublicAPI: !isInstalledApp,
},
)
2025-04-15 15:37:08 +08:00
}, [chatList, handleNewConversationCompleted, handleSend, currentConversationId, currentConversationInputs, newConversationInputs, isInstalledApp, appId])
2025-04-15 15:37:08 +08:00
const doRegenerate = useCallback((chatItem: ChatItemInTree, editedQuestion?: { message: string, files?: FileEntity[] }) => {
const question = editedQuestion ? chatItem : chatList.find(item => item.id === chatItem.parentMessageId)!
const parentAnswer = chatList.find(item => item.id === question.parentMessageId)
2025-04-15 15:37:08 +08:00
doSend(editedQuestion ? editedQuestion.message : question.content,
editedQuestion ? editedQuestion.files : question.message_files,
true,
isValidGeneratedAnswer(parentAnswer) ? parentAnswer : null,
)
}, [chatList, doSend])
2025-03-03 14:44:51 +08:00
const messageList = useMemo(() => {
if (currentConversationId)
return chatList
return chatList.filter(item => !item.isOpeningStatement)
}, [chatList, currentConversationId])
const [collapsed, setCollapsed] = useState(!!currentConversationId)
2024-02-04 16:10:46 +08:00
const chatNode = useMemo(() => {
if (allInputsHidden || !inputsForms.length)
2025-03-03 14:44:51 +08:00
return null
if (isMobile) {
if (!currentConversationId)
return <InputsForm collapsed={collapsed} setCollapsed={setCollapsed} />
return null
2024-02-04 16:10:46 +08:00
}
2025-03-03 14:44:51 +08:00
else {
return <InputsForm collapsed={collapsed} setCollapsed={setCollapsed} />
}
}, [inputsForms.length, isMobile, currentConversationId, collapsed, allInputsHidden])
2024-02-04 16:10:46 +08:00
2025-03-03 14:44:51 +08:00
const welcome = useMemo(() => {
const welcomeMessage = chatList.find(item => item.isOpeningStatement)
2025-03-13 14:23:41 +08:00
if (respondingState)
return null
2025-03-03 14:44:51 +08:00
if (currentConversationId)
return null
if (!welcomeMessage)
return null
if (!collapsed && inputsForms.length > 0 && !allInputsHidden)
2025-03-03 14:44:51 +08:00
return null
2025-03-13 14:23:41 +08:00
if (welcomeMessage.suggestedQuestions && welcomeMessage.suggestedQuestions?.length > 0) {
return (
<div className='flex min-h-[50vh] items-center justify-center px-4 py-12'>
<div className='flex max-w-[720px] grow gap-4'>
2025-03-13 14:23:41 +08:00
<AppIcon
size='xl'
iconType={appData?.site.icon_type}
icon={appData?.site.icon}
background={appData?.site.icon_background}
imageUrl={appData?.site.icon_url}
/>
<div className='w-0 grow'>
<div className='body-lg-regular grow rounded-2xl bg-chat-bubble-bg px-4 py-3 text-text-primary'>
<Markdown content={welcomeMessage.content} />
<SuggestedQuestions item={welcomeMessage} />
</div>
2025-03-13 14:23:41 +08:00
</div>
</div>
</div>
)
}
2024-02-04 16:10:46 +08:00
return (
<div className={cn('flex h-[50vh] flex-col items-center justify-center gap-3 py-12')}>
2025-03-03 14:44:51 +08:00
<AppIcon
size='xl'
iconType={appData?.site.icon_type}
icon={appData?.site.icon}
background={appData?.site.icon_background}
imageUrl={appData?.site.icon_url}
/>
<div className='max-w-[768px] px-4'>
<Markdown className='!body-2xl-regular !text-text-tertiary' content={welcomeMessage.content} />
</div>
2025-03-03 14:44:51 +08:00
</div>
2024-02-04 16:10:46 +08:00
)
}, [appData?.site.icon, appData?.site.icon_background, appData?.site.icon_type, appData?.site.icon_url, chatList, collapsed, currentConversationId, inputsForms.length, respondingState, allInputsHidden])
2024-02-04 16:10:46 +08:00
2024-10-22 18:24:13 +08:00
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
2024-02-04 16:10:46 +08:00
return (
<div
className='h-full overflow-hidden bg-chatbot-bg'
>
<Chat
appData={appData ?? undefined}
config={appConfig}
2025-03-03 14:44:51 +08:00
chatList={messageList}
2025-03-13 14:23:41 +08:00
isResponding={respondingState}
chatContainerInnerClassName={`mx-auto pt-6 w-full max-w-[768px] ${isMobile && 'px-4'}`}
chatFooterClassName='pb-4'
2025-03-13 14:23:41 +08:00
chatFooterInnerClassName={`mx-auto w-full max-w-[768px] ${isMobile ? 'px-2' : 'px-4'}`}
onSend={doSend}
inputs={currentConversationId ? currentConversationInputs as any : newConversationInputs}
inputsForm={inputsForms}
onRegenerate={doRegenerate}
onStopResponding={handleStop}
2025-03-03 14:44:51 +08:00
chatNode={
<>
{chatNode}
{welcome}
</>
}
allToolIcons={appMeta?.tool_icons || {}}
onFeedback={handleFeedback}
suggestedQuestions={suggestedQuestions}
2024-10-22 18:24:13 +08:00
answerIcon={answerIcon}
hideProcessDetail
themeBuilder={themeBuilder}
switchSibling={siblingMessageId => setTargetMessageId(siblingMessageId)}
2025-03-03 14:44:51 +08:00
inputDisabled={inputDisabled}
isMobile={isMobile}
2025-03-13 14:23:41 +08:00
sidebarCollapseState={sidebarCollapseState}
questionIcon={
initUserVariables?.avatar_url
? <Avatar
avatar={initUserVariables.avatar_url}
name={initUserVariables.name || 'user'}
size={40}
/> : undefined
}
/>
</div>
2024-02-04 16:10:46 +08:00
)
}
export default ChatWrapper