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 return null } else { return } }, [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 (
) } return (
) }, [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) ? : null return (
{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} />
) } export default ChatWrapper