mirror of
https://git.mirrors.martin98.com/https://github.com/langgenius/dify.git
synced 2025-05-24 23:28:32 +08:00
265 lines
9.3 KiB
TypeScript
265 lines
9.3 KiB
TypeScript
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 { useEmbeddedChatbotContext } from './context'
|
|
import { isDify } from './utils'
|
|
import { InputVarType } from '@/app/components/workflow/types'
|
|
import { TransferMethod } from '@/types/app'
|
|
import InputsForm from '@/app/components/base/chat/embedded-chatbot/inputs-form'
|
|
import {
|
|
fetchSuggestedQuestions,
|
|
getUrl,
|
|
stopChatMessageResponding,
|
|
} from '@/service/share'
|
|
import AppIcon from '@/app/components/base/app-icon'
|
|
import LogoAvatar from '@/app/components/base/logo/logo-embedded-chat-avatar'
|
|
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 type { FileEntity } from '../../file-uploader/types'
|
|
|
|
const ChatWrapper = () => {
|
|
const {
|
|
appData,
|
|
appParams,
|
|
appPrevChatList,
|
|
currentConversationId,
|
|
currentConversationItem,
|
|
currentConversationInputs,
|
|
inputsForms,
|
|
newConversationInputs,
|
|
newConversationInputsRef,
|
|
handleNewConversationCompleted,
|
|
isMobile,
|
|
isInstalledApp,
|
|
appId,
|
|
appMeta,
|
|
handleFeedback,
|
|
currentChatInstanceRef,
|
|
themeBuilder,
|
|
clearChatList,
|
|
setClearChatList,
|
|
setIsResponding,
|
|
} = useEmbeddedChatbotContext()
|
|
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 ? currentConversationInputs : newConversationInputs) as any,
|
|
inputsForm: inputsForms,
|
|
},
|
|
appPrevChatList,
|
|
taskId => stopChatMessageResponding('', taskId, isInstalledApp, appId),
|
|
clearChatList,
|
|
setClearChatList,
|
|
)
|
|
const inputsFormValue = currentConversationId ? currentConversationInputs : 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
|
|
}, [currentChatInstanceRef, handleStop])
|
|
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 ? currentConversationInputs : 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,
|
|
},
|
|
)
|
|
}, [currentConversationId, currentConversationInputs, newConversationInputs, chatList, handleSend, isInstalledApp, appId, handleNewConversationCompleted])
|
|
|
|
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)
|
|
doSend(editedQuestion ? editedQuestion.message : question.content,
|
|
editedQuestion ? editedQuestion.files : 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 <div className='mb-4'></div>
|
|
}
|
|
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={cn('flex items-center justify-center px-4 py-12', isMobile ? 'min-h-[30vh] py-0' : 'h-[50vh]')}>
|
|
<div className='flex max-w-[720px] grow 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='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>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
return (
|
|
<div className={cn('flex h-[50vh] flex-col items-center justify-center gap-3 py-12', isMobile ? 'min-h-[30vh] py-0' : 'h-[50vh]')}>
|
|
<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>
|
|
</div>
|
|
)
|
|
}, [appData?.site.icon, appData?.site.icon_background, appData?.site.icon_type, appData?.site.icon_url, chatList, collapsed, currentConversationId, inputsForms.length, respondingState])
|
|
|
|
const answerIcon = isDify()
|
|
? <LogoAvatar className='relative shrink-0' />
|
|
: (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 (
|
|
<Chat
|
|
appData={appData}
|
|
config={appConfig}
|
|
chatList={messageList}
|
|
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')}
|
|
chatFooterInnerClassName={cn('mx-auto w-full max-w-full px-4', isMobile && 'px-2')}
|
|
onSend={doSend}
|
|
inputs={currentConversationId ? currentConversationInputs 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}
|
|
/>
|
|
)
|
|
}
|
|
|
|
export default ChatWrapper
|