|
|
|
@@ -1,40 +1,39 @@ |
|
|
|
/* eslint-disable @typescript-eslint/no-use-before-define */ |
|
|
|
'use client' |
|
|
|
import type { FC } from 'react' |
|
|
|
import React, { useEffect, useState, useRef } from 'react' |
|
|
|
import React, { useEffect, useRef, useState } from 'react' |
|
|
|
import cn from 'classnames' |
|
|
|
import { useTranslation } from 'react-i18next' |
|
|
|
import { useContext } from 'use-context-selector' |
|
|
|
import produce from 'immer' |
|
|
|
import { useBoolean, useGetState } from 'ahooks' |
|
|
|
import AppUnavailable from '../../base/app-unavailable' |
|
|
|
import useConversation from './hooks/use-conversation' |
|
|
|
import s from './style.module.css' |
|
|
|
import { ToastContext } from '@/app/components/base/toast' |
|
|
|
import Sidebar from '@/app/components/share/chat/sidebar' |
|
|
|
import ConfigSence from '@/app/components/share/chat/config-scence' |
|
|
|
import Header from '@/app/components/share/header' |
|
|
|
import { fetchAppInfo, fetchAppParams, fetchChatList, fetchConversations, sendChatMessage, updateFeedback, fetchSuggestedQuestions } from '@/service/share' |
|
|
|
import { fetchAppInfo, fetchAppParams, fetchChatList, fetchConversations, fetchSuggestedQuestions, sendChatMessage, updateFeedback } from '@/service/share' |
|
|
|
import type { ConversationItem, SiteInfo } from '@/models/share' |
|
|
|
import type { PromptConfig } from '@/models/debug' |
|
|
|
import type { PromptConfig, SuggestedQuestionsAfterAnswerConfig } from '@/models/debug' |
|
|
|
import type { Feedbacktype, IChatItem } from '@/app/components/app/chat' |
|
|
|
import Chat from '@/app/components/app/chat' |
|
|
|
import { changeLanguage } from '@/i18n/i18next-config' |
|
|
|
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' |
|
|
|
import Loading from '@/app/components/base/loading' |
|
|
|
import { replaceStringWithValues } from '@/app/components/app/configuration/prompt-value-panel' |
|
|
|
import AppUnavailable from '../../base/app-unavailable' |
|
|
|
import { userInputsFormToPromptVariables } from '@/utils/model-config' |
|
|
|
import { SuggestedQuestionsAfterAnswerConfig } from '@/models/debug' |
|
|
|
import { InstalledApp } from '@/models/explore' |
|
|
|
|
|
|
|
import s from './style.module.css' |
|
|
|
import type { InstalledApp } from '@/models/explore' |
|
|
|
|
|
|
|
export type IMainProps = { |
|
|
|
isInstalledApp?: boolean, |
|
|
|
installedAppInfo? : InstalledApp |
|
|
|
isInstalledApp?: boolean |
|
|
|
installedAppInfo?: InstalledApp |
|
|
|
} |
|
|
|
|
|
|
|
const Main: FC<IMainProps> = ({ |
|
|
|
isInstalledApp = false, |
|
|
|
installedAppInfo |
|
|
|
installedAppInfo, |
|
|
|
}) => { |
|
|
|
const { t } = useTranslation() |
|
|
|
const media = useBreakpoints() |
|
|
|
@@ -53,7 +52,7 @@ const Main: FC<IMainProps> = ({ |
|
|
|
const [plan, setPlan] = useState<string>('basic') // basic/plus/pro |
|
|
|
// in mobile, show sidebar by click button |
|
|
|
const [isShowSidebar, { setTrue: showSidebar, setFalse: hideSidebar }] = useBoolean(false) |
|
|
|
// Can Use metadata(https://beta.nextjs.org/docs/api-reference/metadata) to set title. But it only works in server side client. |
|
|
|
// Can Use metadata(https://beta.nextjs.org/docs/api-reference/metadata) to set title. But it only works in server side client. |
|
|
|
useEffect(() => { |
|
|
|
if (siteInfo?.title) { |
|
|
|
if (plan !== 'basic') |
|
|
|
@@ -61,7 +60,6 @@ const Main: FC<IMainProps> = ({ |
|
|
|
else |
|
|
|
document.title = `${siteInfo.title} - Powered by Dify` |
|
|
|
} |
|
|
|
|
|
|
|
}, [siteInfo?.title, plan]) |
|
|
|
|
|
|
|
/* |
|
|
|
@@ -81,7 +79,7 @@ const Main: FC<IMainProps> = ({ |
|
|
|
resetNewConversationInputs, |
|
|
|
setCurrInputs, |
|
|
|
setNewConversationInfo, |
|
|
|
setExistConversationInfo |
|
|
|
setExistConversationInfo, |
|
|
|
} = useConversation() |
|
|
|
const [hasMore, setHasMore] = useState<boolean>(false) |
|
|
|
const onMoreLoaded = ({ data: conversations, has_more }: any) => { |
|
|
|
@@ -101,9 +99,9 @@ const Main: FC<IMainProps> = ({ |
|
|
|
setChatList(generateNewChatListWithOpenstatement('', inputs)) |
|
|
|
} |
|
|
|
const hasSetInputs = (() => { |
|
|
|
if (!isNewConversation) { |
|
|
|
if (!isNewConversation) |
|
|
|
return true |
|
|
|
} |
|
|
|
|
|
|
|
return isChatStarted |
|
|
|
})() |
|
|
|
|
|
|
|
@@ -111,7 +109,8 @@ const Main: FC<IMainProps> = ({ |
|
|
|
const conversationIntroduction = currConversationInfo?.introduction || '' |
|
|
|
|
|
|
|
const handleConversationSwitch = () => { |
|
|
|
if (!inited) return |
|
|
|
if (!inited) |
|
|
|
return |
|
|
|
if (!appId) { |
|
|
|
// wait for appId |
|
|
|
setTimeout(handleConversationSwitch, 100) |
|
|
|
@@ -130,12 +129,13 @@ const Main: FC<IMainProps> = ({ |
|
|
|
name: item?.name || '', |
|
|
|
introduction: notSyncToStateIntroduction, |
|
|
|
}) |
|
|
|
} else { |
|
|
|
} |
|
|
|
else { |
|
|
|
notSyncToStateInputs = newConversationInputs |
|
|
|
setCurrInputs(notSyncToStateInputs) |
|
|
|
} |
|
|
|
|
|
|
|
// update chat list of current conversation |
|
|
|
// update chat list of current conversation |
|
|
|
if (!isNewConversation && !conversationIdChangeBecauseOfNew && !isResponsing) { |
|
|
|
fetchChatList(currConversationId, isInstalledApp, installedAppInfo?.id).then((res: any) => { |
|
|
|
const { data } = res |
|
|
|
@@ -158,9 +158,8 @@ const Main: FC<IMainProps> = ({ |
|
|
|
}) |
|
|
|
} |
|
|
|
|
|
|
|
if (isNewConversation && isChatStarted) { |
|
|
|
if (isNewConversation && isChatStarted) |
|
|
|
setChatList(generateNewChatListWithOpenstatement()) |
|
|
|
} |
|
|
|
|
|
|
|
setControlFocus(Date.now()) |
|
|
|
} |
|
|
|
@@ -170,7 +169,8 @@ const Main: FC<IMainProps> = ({ |
|
|
|
if (id === '-1') { |
|
|
|
createNewChat() |
|
|
|
setConversationIdChangeBecauseOfNew(true) |
|
|
|
} else { |
|
|
|
} |
|
|
|
else { |
|
|
|
setConversationIdChangeBecauseOfNew(false) |
|
|
|
} |
|
|
|
// trigger handleConversationSwitch |
|
|
|
@@ -186,9 +186,8 @@ const Main: FC<IMainProps> = ({ |
|
|
|
const chatListDomRef = useRef<HTMLDivElement>(null) |
|
|
|
useEffect(() => { |
|
|
|
// scroll to bottom |
|
|
|
if (chatListDomRef.current) { |
|
|
|
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 |
|
|
|
@@ -196,15 +195,15 @@ const Main: FC<IMainProps> = ({ |
|
|
|
// if new chat is already exist, do not create new chat |
|
|
|
abortController?.abort() |
|
|
|
setResponsingFalse() |
|
|
|
if (conversationList.some(item => item.id === '-1')) { |
|
|
|
if (conversationList.some(item => item.id === '-1')) |
|
|
|
return |
|
|
|
} |
|
|
|
setConversationList(produce(conversationList, draft => { |
|
|
|
|
|
|
|
setConversationList(produce(conversationList, (draft) => { |
|
|
|
draft.unshift({ |
|
|
|
id: '-1', |
|
|
|
name: t('share.chat.newChatDefaultName'), |
|
|
|
inputs: newConversationInputs, |
|
|
|
introduction: conversationIntroduction |
|
|
|
introduction: conversationIntroduction, |
|
|
|
}) |
|
|
|
})) |
|
|
|
} |
|
|
|
@@ -213,36 +212,37 @@ const Main: FC<IMainProps> = ({ |
|
|
|
const generateNewChatListWithOpenstatement = (introduction?: string, inputs?: Record<string, any> | null) => { |
|
|
|
let caculatedIntroduction = introduction || conversationIntroduction || '' |
|
|
|
const caculatedPromptVariables = inputs || currInputs || null |
|
|
|
if (caculatedIntroduction && caculatedPromptVariables) { |
|
|
|
if (caculatedIntroduction && caculatedPromptVariables) |
|
|
|
caculatedIntroduction = replaceStringWithValues(caculatedIntroduction, promptConfig?.prompt_variables || [], caculatedPromptVariables) |
|
|
|
} |
|
|
|
|
|
|
|
// console.log(isPublicVersion) |
|
|
|
const openstatement = { |
|
|
|
id: `${Date.now()}`, |
|
|
|
content: caculatedIntroduction, |
|
|
|
isAnswer: true, |
|
|
|
feedbackDisabled: true, |
|
|
|
isOpeningStatement: isPublicVersion |
|
|
|
isOpeningStatement: isPublicVersion, |
|
|
|
} |
|
|
|
if (caculatedIntroduction) { |
|
|
|
if (caculatedIntroduction) |
|
|
|
return [openstatement] |
|
|
|
} |
|
|
|
|
|
|
|
return [] |
|
|
|
} |
|
|
|
|
|
|
|
const fetchInitData = () => { |
|
|
|
return Promise.all([isInstalledApp ? { |
|
|
|
app_id: installedAppInfo?.id, |
|
|
|
site: { |
|
|
|
title: installedAppInfo?.app.name, |
|
|
|
prompt_public: false, |
|
|
|
copyright: '' |
|
|
|
}, |
|
|
|
plan: 'basic', |
|
|
|
}: fetchAppInfo(), fetchConversations(isInstalledApp, installedAppInfo?.id), fetchAppParams(isInstalledApp, installedAppInfo?.id)]) |
|
|
|
return Promise.all([isInstalledApp |
|
|
|
? { |
|
|
|
app_id: installedAppInfo?.id, |
|
|
|
site: { |
|
|
|
title: installedAppInfo?.app.name, |
|
|
|
prompt_public: false, |
|
|
|
copyright: '', |
|
|
|
}, |
|
|
|
plan: 'basic', |
|
|
|
} |
|
|
|
: fetchAppInfo(), fetchConversations(isInstalledApp, installedAppInfo?.id), fetchAppParams(isInstalledApp, installedAppInfo?.id)]) |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// init |
|
|
|
useEffect(() => { |
|
|
|
(async () => { |
|
|
|
@@ -255,16 +255,16 @@ const Main: FC<IMainProps> = ({ |
|
|
|
setIsPublicVersion(tempIsPublicVersion) |
|
|
|
const prompt_template = '' |
|
|
|
// handle current conversation id |
|
|
|
const { data: conversations, has_more } = conversationData as { data: ConversationItem[], has_more: boolean } |
|
|
|
const { data: conversations, has_more } = conversationData as { data: ConversationItem[]; has_more: boolean } |
|
|
|
const _conversationId = getConversationIdFromStorage(appId) |
|
|
|
const isNotNewConversation = conversations.some(item => item.id === _conversationId) |
|
|
|
setHasMore(has_more) |
|
|
|
// fetch new conversation info |
|
|
|
const { user_input_form, opening_statement: introduction, suggested_questions_after_answer }: any = appParams |
|
|
|
const prompt_variables = userInputsFormToPromptVariables(user_input_form) |
|
|
|
if(siteInfo.default_language) { |
|
|
|
if (siteInfo.default_language) |
|
|
|
changeLanguage(siteInfo.default_language) |
|
|
|
} |
|
|
|
|
|
|
|
setNewConversationInfo({ |
|
|
|
name: t('share.chat.newChatDefaultName'), |
|
|
|
introduction, |
|
|
|
@@ -272,20 +272,22 @@ const Main: FC<IMainProps> = ({ |
|
|
|
setSiteInfo(siteInfo as SiteInfo) |
|
|
|
setPromptConfig({ |
|
|
|
prompt_template, |
|
|
|
prompt_variables: prompt_variables, |
|
|
|
prompt_variables, |
|
|
|
} as PromptConfig) |
|
|
|
setSuggestedQuestionsAfterAnswerConfig(suggested_questions_after_answer) |
|
|
|
|
|
|
|
setConversationList(conversations as ConversationItem[]) |
|
|
|
|
|
|
|
if (isNotNewConversation) { |
|
|
|
if (isNotNewConversation) |
|
|
|
setCurrConversationId(_conversationId, appId, false) |
|
|
|
} |
|
|
|
|
|
|
|
setInited(true) |
|
|
|
} catch (e: any) { |
|
|
|
} |
|
|
|
catch (e: any) { |
|
|
|
if (e.status === 404) { |
|
|
|
setAppUnavailable(true) |
|
|
|
} else { |
|
|
|
} |
|
|
|
else { |
|
|
|
setIsUnknwonReason(true) |
|
|
|
setAppUnavailable(true) |
|
|
|
} |
|
|
|
@@ -303,21 +305,20 @@ const Main: FC<IMainProps> = ({ |
|
|
|
const checkCanSend = () => { |
|
|
|
const prompt_variables = promptConfig?.prompt_variables |
|
|
|
const inputs = currInputs |
|
|
|
if (!inputs || !prompt_variables || prompt_variables?.length === 0) { |
|
|
|
if (!inputs || !prompt_variables || prompt_variables?.length === 0) |
|
|
|
return true |
|
|
|
} |
|
|
|
|
|
|
|
let hasEmptyInput = false |
|
|
|
const requiredVars = prompt_variables?.filter(({ key, name, required }) => { |
|
|
|
const res = (!key || !key.trim()) || (!name || !name.trim()) || (required || required === undefined || required === null) |
|
|
|
return res |
|
|
|
}) || [] // compatible with old version |
|
|
|
requiredVars.forEach(({ key }) => { |
|
|
|
if (hasEmptyInput) { |
|
|
|
if (hasEmptyInput) |
|
|
|
return |
|
|
|
} |
|
|
|
if (!inputs?.[key]) { |
|
|
|
|
|
|
|
if (!inputs?.[key]) |
|
|
|
hasEmptyInput = true |
|
|
|
} |
|
|
|
}) |
|
|
|
|
|
|
|
if (hasEmptyInput) { |
|
|
|
@@ -378,9 +379,8 @@ const Main: FC<IMainProps> = ({ |
|
|
|
onData: (message: string, isFirstMessage: boolean, { conversationId: newConversationId, messageId }: any) => { |
|
|
|
responseItem.content = responseItem.content + message |
|
|
|
responseItem.id = messageId |
|
|
|
if (isFirstMessage && newConversationId) { |
|
|
|
if (isFirstMessage && newConversationId) |
|
|
|
tempNewConversationId = newConversationId |
|
|
|
} |
|
|
|
|
|
|
|
// closesure new list is outdated. |
|
|
|
const newListWithAnswer = produce( |
|
|
|
@@ -395,9 +395,9 @@ const Main: FC<IMainProps> = ({ |
|
|
|
}, |
|
|
|
async onCompleted(hasError?: boolean) { |
|
|
|
setResponsingFalse() |
|
|
|
if (hasError) { |
|
|
|
if (hasError) |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
let currChatList = conversationList |
|
|
|
if (getConversationIdChangeBecauseOfNew()) { |
|
|
|
const { data: conversations, has_more }: any = await fetchConversations(isInstalledApp, installedAppInfo?.id) |
|
|
|
@@ -418,7 +418,7 @@ const Main: FC<IMainProps> = ({ |
|
|
|
onError() { |
|
|
|
setResponsingFalse() |
|
|
|
// role back placeholder answer |
|
|
|
setChatList(produce(getChatList(), draft => { |
|
|
|
setChatList(produce(getChatList(), (draft) => { |
|
|
|
draft.splice(draft.findIndex(item => item.id === placeholderAnswerId), 1) |
|
|
|
})) |
|
|
|
}, |
|
|
|
@@ -476,18 +476,20 @@ const Main: FC<IMainProps> = ({ |
|
|
|
onCreateNewChat={() => handleConversationIdChange('-1')} |
|
|
|
/> |
|
|
|
)} |
|
|
|
|
|
|
|
|
|
|
|
{/* {isNewConversation ? 'new' : 'exist'} |
|
|
|
{JSON.stringify(newConversationInputs ? newConversationInputs : {})} |
|
|
|
{JSON.stringify(existConversationInputs ? existConversationInputs : {})} */} |
|
|
|
<div |
|
|
|
<div |
|
|
|
className={cn( |
|
|
|
"flex rounded-t-2xl bg-white overflow-hidden", |
|
|
|
'flex rounded-t-2xl bg-white overflow-hidden', |
|
|
|
isInstalledApp && 'rounded-b-2xl', |
|
|
|
)} |
|
|
|
style={isInstalledApp ? { |
|
|
|
boxShadow: '0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03)' |
|
|
|
} : {}} |
|
|
|
style={isInstalledApp |
|
|
|
? { |
|
|
|
boxShadow: '0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03)', |
|
|
|
} |
|
|
|
: {}} |
|
|
|
> |
|
|
|
{/* sidebar */} |
|
|
|
{!isMobile && renderSidebar()} |
|
|
|
@@ -504,8 +506,8 @@ const Main: FC<IMainProps> = ({ |
|
|
|
{/* main */} |
|
|
|
<div className={cn( |
|
|
|
isInstalledApp ? s.installedApp : 'h-[calc(100vh_-_3rem)]', |
|
|
|
'flex-grow flex flex-col overflow-y-auto' |
|
|
|
) |
|
|
|
'flex-grow flex flex-col overflow-y-auto', |
|
|
|
) |
|
|
|
}> |
|
|
|
<ConfigSence |
|
|
|
conversationName={conversationName} |
|
|
|
@@ -522,7 +524,7 @@ const Main: FC<IMainProps> = ({ |
|
|
|
|
|
|
|
{ |
|
|
|
hasSetInputs && ( |
|
|
|
<div className={cn(doShowSuggestion ? 'pb-[140px]' : 'pb-[66px]', 'relative grow h-[200px] pc:w-[794px] max-w-full mobile:w-full mx-auto mb-3.5 overflow-hidden')}> |
|
|
|
<div className={cn(doShowSuggestion ? 'pb-[140px]' : (isResponsing ? 'pb-[113px]' : 'pb-[66px]'), 'relative grow h-[200px] pc:w-[794px] max-w-full mobile:w-full mx-auto mb-3.5 overflow-hidden')}> |
|
|
|
<div className='h-full overflow-y-auto' ref={chatListDomRef}> |
|
|
|
<Chat |
|
|
|
chatList={chatList} |