| '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 |
|
|
| |
| |
| |
| const [appUnavailable, setAppUnavailable] = useState<boolean>(false) |
| const [isUnknwonReason, setIsUnknwonReason] = useState<boolean>(false) |
| const [promptConfig, setPromptConfig] = useState<PromptConfig | null>(null) |
| const [inited, setInited] = useState<boolean>(false) |
| |
| const [isShowSidebar, { setTrue: showSidebar, setFalse: hideSidebar }] = useBoolean(false) |
|
|
| useEffect(() => { |
| if (APP_INFO?.title) { |
| document.title = `${APP_INFO.title} - Powered by Dify` |
| } |
| }, [APP_INFO?.title]) |
|
|
| |
| |
| |
| 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<string, any>) => { |
| setCurrInputs(inputs) |
| setChatStarted() |
| |
| 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 |
|
|
| |
| let notSyncToStateIntroduction = '' |
| let notSyncToStateInputs: Record<string, any> | 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) |
| } |
|
|
| |
| 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) |
| } |
| |
| setCurrConversationId(id, APP_ID) |
| hideSidebar() |
| } |
|
|
| |
| |
| |
| const [chatList, setChatList, getChatList] = useGetState<IChatItem[]>([]) |
| const chatListDomRef = useRef<HTMLDivElement>(null) |
| useEffect(() => { |
| |
| if (chatListDomRef.current) |
| chatListDomRef.current.scrollTop = chatListDomRef.current.scrollHeight |
| }, [chatList, currConversationId]) |
| |
| const canEditInpus = !chatList.some(item => item.isAnswer === false) && isNewConversation |
| const createNewChat = () => { |
| |
| 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, |
| }) |
| })) |
| } |
|
|
| |
| const generateNewChatListWithOpenstatement = (introduction?: string, inputs?: Record<string, any> | 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 [] |
| } |
|
|
| |
| useEffect(() => { |
| if (!hasSetAppConfig) { |
| setAppUnavailable(true) |
| return |
| } |
| (async () => { |
| try { |
| const [conversationData, appParams] = await Promise.all([fetchConversations(), fetchAppParams()]) |
|
|
| |
| const { data: conversations } = conversationData as { data: ConversationItem[] } |
| const _conversationId = getConversationIdFromStorage(APP_ID) |
| const isNotNewConversation = conversations.some(item => item.id === _conversationId) |
|
|
| |
| 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, |
| } |
|
|
| |
| 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) |
|
|
| |
| 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 |
|
|
| |
| 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() |
| |
| 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 ( |
| <Sidebar |
| list={conversationList} |
| onCurrentIdChange={handleConversationIdChange} |
| currentId={currConversationId} |
| copyRight={APP_INFO.copyright || APP_INFO.title} |
| /> |
| ) |
| } |
|
|
| if (appUnavailable) |
| return <AppUnavailable isUnknwonReason={isUnknwonReason} errMessage={!hasSetAppConfig ? 'Please set APP_ID and API_KEY in config/index.tsx' : ''} /> |
|
|
| if (!APP_ID || !APP_INFO || !promptConfig) |
| return <Loading type='app' /> |
|
|
| return ( |
| <div className='bg-gray-100'> |
| <Header |
| title={APP_INFO.title} |
| isMobile={isMobile} |
| onShowSideBar={showSidebar} |
| onCreateNewChat={() => handleConversationIdChange('-1')} |
| /> |
| <div className="flex rounded-t-2xl bg-white overflow-hidden"> |
| {/* sidebar */} |
| {!isMobile && renderSidebar()} |
| {isMobile && isShowSidebar && ( |
| <div className='fixed inset-0 z-50' |
| style={{ backgroundColor: 'rgba(35, 56, 118, 0.2)' }} |
| onClick={hideSidebar} |
| > |
| <div className='inline-block' onClick={e => e.stopPropagation()}> |
| {renderSidebar()} |
| </div> |
| </div> |
| )} |
| {/* main */} |
| <div className='flex-grow flex flex-col h-[calc(100vh_-_3rem)] overflow-y-auto'> |
| <ConfigSence |
| conversationName={conversationName} |
| hasSetInputs={hasSetInputs} |
| isPublicVersion={isShowPrompt} |
| siteInfo={APP_INFO} |
| promptConfig={promptConfig} |
| onStartChat={handleStartChat} |
| canEidtInpus={canEditInpus} |
| savedInputs={currInputs as Record<string, any>} |
| onInputsChange={setCurrInputs} |
| ></ConfigSence> |
| |
| { |
| hasSetInputs && ( |
| <div className='relative grow h-[200px] pc:w-[794px] max-w-full mobile:w-full pb-[66px] mx-auto mb-3.5 overflow-hidden'> |
| <div className='h-full overflow-y-auto' ref={chatListDomRef}> |
| <Chat |
| chatList={chatList} |
| onSend={handleSend} |
| onFeedback={handleFeedback} |
| isResponsing={isResponsing} |
| checkCanSend={checkCanSend} |
| controlFocus={controlFocus} |
| /> |
| </div> |
| </div>) |
| } |
| </div> |
| </div> |
| </div> |
| ) |
| } |
| |
| export default React.memo(Main) |
| |