'use client' import type { FC } from 'react' import React, { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import produce from 'immer' import { useBoolean, useGetState } from 'ahooks' import useConversation from '@/hooks/use-conversation' import Toast from '@/app/components/base/toast' import Sidebar from '@/app/components/sidebar' import ConfigSence from '@/app/components/config-scence' import Header from '@/app/components/header' import { fetchAppParams, fetchChatList, fetchConversations, sendChatMessage, updateFeedback } from '@/service' import type { ConversationItem, Feedbacktype, IChatItem, PromptConfig, AppInfo } from '@/types/app' import Chat from '@/app/components/chat' import { setLocaleOnClient } from '@/i18n/client' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import Loading from '@/app/components/base/loading' import { replaceVarWithValues } from '@/utils/prompt' import AppUnavailable from '@/app/components/app-unavailable' import { APP_ID, API_KEY, APP_INFO, isShowPrompt, promptTemplate } from '@/config' import { userInputsFormToPromptVariables } from '@/utils/prompt' const Main: FC = () => { const { t } = useTranslation() const media = useBreakpoints() const isMobile = media === MediaType.mobile const hasSetAppConfig = APP_ID && API_KEY /* * app info */ const [appUnavailable, setAppUnavailable] = useState(false) const [isUnknwonReason, setIsUnknwonReason] = useState(false) const [promptConfig, setPromptConfig] = useState(null) const [inited, setInited] = useState(false) // in mobile, show sidebar by click button const [isShowSidebar, { setTrue: showSidebar, setFalse: hideSidebar }] = useBoolean(false) useEffect(() => { if (APP_INFO?.title) { document.title = `${APP_INFO.title} - Powered by Dify` } }, [APP_INFO?.title]) /* * conversation info */ const { conversationList, setConversationList, currConversationId, setCurrConversationId, getConversationIdFromStorage, isNewConversation, currConversationInfo, currInputs, newConversationInputs, resetNewConversationInputs, setCurrInputs, setNewConversationInfo, setExistConversationInfo, } = useConversation() const [conversationIdChangeBecauseOfNew, setConversationIdChangeBecauseOfNew, getConversationIdChangeBecauseOfNew] = useGetState(false) const [isChatStarted, { setTrue: setChatStarted, setFalse: setChatNotStarted }] = useBoolean(false) const handleStartChat = (inputs: Record) => { setCurrInputs(inputs) setChatStarted() // parse variables in introduction setChatList(generateNewChatListWithOpenstatement('', inputs)) } const hasSetInputs = (() => { if (!isNewConversation) return true return isChatStarted })() const conversationName = currConversationInfo?.name || t('app.chat.newChatDefaultName') as string const conversationIntroduction = currConversationInfo?.introduction || '' const handleConversationSwitch = () => { if (!inited) return // update inputs of current conversation let notSyncToStateIntroduction = '' let notSyncToStateInputs: Record | undefined | null = {} if (!isNewConversation) { const item = conversationList.find(item => item.id === currConversationId) notSyncToStateInputs = item?.inputs || {} setCurrInputs(notSyncToStateInputs as any) notSyncToStateIntroduction = item?.introduction || '' setExistConversationInfo({ name: item?.name || '', introduction: notSyncToStateIntroduction, }) } else { notSyncToStateInputs = newConversationInputs setCurrInputs(notSyncToStateInputs) } // update chat list of current conversation if (!isNewConversation && !conversationIdChangeBecauseOfNew && !isResponsing) { fetchChatList(currConversationId).then((res: any) => { const { data } = res const newChatList: IChatItem[] = generateNewChatListWithOpenstatement(notSyncToStateIntroduction, notSyncToStateInputs) data.forEach((item: any) => { newChatList.push({ id: `question-${item.id}`, content: item.query, isAnswer: false, }) newChatList.push({ id: item.id, content: item.answer, feedback: item.feedback, isAnswer: true, }) }) setChatList(newChatList) }) } if (isNewConversation && isChatStarted) setChatList(generateNewChatListWithOpenstatement()) setControlFocus(Date.now()) } useEffect(handleConversationSwitch, [currConversationId, inited]) const handleConversationIdChange = (id: string) => { if (id === '-1') { createNewChat() setConversationIdChangeBecauseOfNew(true) } else { setConversationIdChangeBecauseOfNew(false) } // trigger handleConversationSwitch setCurrConversationId(id, APP_ID) hideSidebar() } /* * chat info. chat is under conversation. */ const [chatList, setChatList, getChatList] = useGetState([]) const chatListDomRef = useRef(null) useEffect(() => { // scroll to bottom if (chatListDomRef.current) chatListDomRef.current.scrollTop = chatListDomRef.current.scrollHeight }, [chatList, currConversationId]) // user can not edit inputs if user had send message const canEditInpus = !chatList.some(item => item.isAnswer === false) && isNewConversation const createNewChat = () => { // if new chat is already exist, do not create new chat if (conversationList.some(item => item.id === '-1')) return setConversationList(produce(conversationList, (draft) => { draft.unshift({ id: '-1', name: t('app.chat.newChatDefaultName'), inputs: newConversationInputs, introduction: conversationIntroduction, }) })) } // sometime introduction is not applied to state const generateNewChatListWithOpenstatement = (introduction?: string, inputs?: Record | null) => { let caculatedIntroduction = introduction || conversationIntroduction || '' const caculatedPromptVariables = inputs || currInputs || null if (caculatedIntroduction && caculatedPromptVariables) caculatedIntroduction = replaceVarWithValues(caculatedIntroduction, promptConfig?.prompt_variables || [], caculatedPromptVariables) const openstatement = { id: `${Date.now()}`, content: caculatedIntroduction, isAnswer: true, feedbackDisabled: true, isOpeningStatement: isShowPrompt, } if (caculatedIntroduction) return [openstatement] return [] } // init useEffect(() => { if (!hasSetAppConfig) { setAppUnavailable(true) return } (async () => { try { const [conversationData, appParams] = await Promise.all([fetchConversations(), fetchAppParams()]) // handle current conversation id const { data: conversations } = conversationData as { data: ConversationItem[] } const _conversationId = getConversationIdFromStorage(APP_ID) const isNotNewConversation = conversations.some(item => item.id === _conversationId) // fetch new conversation info const { user_input_form, opening_statement: introduction }: any = appParams setLocaleOnClient(APP_INFO.default_language, true) setNewConversationInfo({ name: t('app.chat.newChatDefaultName'), introduction, }) const prompt_variables = userInputsFormToPromptVariables(user_input_form) setPromptConfig({ prompt_template: promptTemplate, prompt_variables, } as PromptConfig) setConversationList(conversations as ConversationItem[]) if (isNotNewConversation) setCurrConversationId(_conversationId, APP_ID, false) setInited(true) } catch (e: any) { if (e.status === 404) { setAppUnavailable(true) } else { setIsUnknwonReason(true) setAppUnavailable(true) } } })() }, []) const [isResponsing, { setTrue: setResponsingTrue, setFalse: setResponsingFalse }] = useBoolean(false) const { notify } = Toast const logError = (message: string) => { notify({ type: 'error', message }) } const checkCanSend = () => { if (!currInputs || !promptConfig?.prompt_variables) return true const inputLens = Object.values(currInputs).length const promptVariablesLens = promptConfig.prompt_variables.length const emytyInput = inputLens < promptVariablesLens || Object.values(currInputs).find(v => !v) if (emytyInput) { logError(t('app.errorMessage.valueOfVarRequired')) return false } return true } const [controlFocus, setControlFocus] = useState(0) const handleSend = async (message: string) => { if (isResponsing) { notify({ type: 'info', message: t('app.errorMessage.waitForResponse') }) return } const data = { inputs: currInputs, query: message, conversation_id: isNewConversation ? null : currConversationId, } // qustion const questionId = `question-${Date.now()}` const questionItem = { id: questionId, content: message, isAnswer: false, } const placeholderAnswerId = `answer-placeholder-${Date.now()}` const placeholderAnswerItem = { id: placeholderAnswerId, content: '', isAnswer: true, } const newList = [...getChatList(), questionItem, placeholderAnswerItem] setChatList(newList) // answer const responseItem = { id: `${Date.now()}`, content: '', isAnswer: true, } let tempNewConversationId = '' setResponsingTrue() sendChatMessage(data, { onData: (message: string, isFirstMessage: boolean, { conversationId: newConversationId, messageId }: any) => { responseItem.content = responseItem.content + message responseItem.id = messageId if (isFirstMessage && newConversationId) tempNewConversationId = newConversationId // closesure new list is outdated. const newListWithAnswer = produce( getChatList().filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId), (draft) => { if (!draft.find(item => item.id === questionId)) draft.push({ ...questionItem }) draft.push({ ...responseItem }) }) setChatList(newListWithAnswer) }, async onCompleted() { setResponsingFalse() if (!tempNewConversationId) { return } if (getConversationIdChangeBecauseOfNew()) { const { data: conversations }: any = await fetchConversations() setConversationList(conversations as ConversationItem[]) } setConversationIdChangeBecauseOfNew(false) resetNewConversationInputs() setChatNotStarted() setCurrConversationId(tempNewConversationId, APP_ID, true) }, onError() { setResponsingFalse() // role back placeholder answer setChatList(produce(getChatList(), (draft) => { draft.splice(draft.findIndex(item => item.id === placeholderAnswerId), 1) })) }, }) } const handleFeedback = async (messageId: string, feedback: Feedbacktype) => { await updateFeedback({ url: `/messages/${messageId}/feedbacks`, body: { rating: feedback.rating } }) const newChatList = chatList.map((item) => { if (item.id === messageId) { return { ...item, feedback, } } return item }) setChatList(newChatList) notify({ type: 'success', message: t('common.api.success') }) } const renderSidebar = () => { if (!APP_ID || !APP_INFO || !promptConfig) return null return ( ) } if (appUnavailable) return if (!APP_ID || !APP_INFO || !promptConfig) return return (
handleConversationIdChange('-1')} />
{/* sidebar */} {!isMobile && renderSidebar()} {isMobile && isShowSidebar && (
e.stopPropagation()}> {renderSidebar()}
)} {/* main */}
} onInputsChange={setCurrInputs} > { hasSetInputs && (
) }
) } export default React.memo(Main)