feat: support assistant frontend (#2139)

Co-authored-by: StyleZhang <jasonapring2015@outlook.com>
This commit is contained in:
Joel
2024-01-23 19:31:56 +08:00
committed by GitHub
parent e65a2a400d
commit 7bbe12b2bd
194 changed files with 8726 additions and 1586 deletions

View File

@@ -6,10 +6,11 @@ import cn from 'classnames'
import useSWR from 'swr'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import produce from 'immer'
import produce, { setAutoFreeze } from 'immer'
import { useBoolean, useGetState } from 'ahooks'
import AppUnavailable from '../../base/app-unavailable'
import { checkOrSetAccessToken } from '../utils'
import { addFileInfos, sortAgentSorts } from '../../tools/utils'
import useConversation from './hooks/use-conversation'
import { ToastContext } from '@/app/components/base/toast'
import Sidebar from '@/app/components/share/chat/sidebar'
@@ -18,6 +19,7 @@ import Header from '@/app/components/share/header'
import {
delConversation,
fetchAppInfo,
fetchAppMeta,
fetchAppParams,
fetchChatList,
fetchConversations,
@@ -29,7 +31,7 @@ import {
unpinConversation,
updateFeedback,
} from '@/service/share'
import type { ConversationItem, SiteInfo } from '@/models/share'
import type { AppMeta, ConversationItem, SiteInfo } from '@/models/share'
import type { PromptConfig, SuggestedQuestionsAfterAnswerConfig } from '@/models/debug'
import type { Feedbacktype, IChatItem } from '@/app/components/app/chat/type'
import Chat from '@/app/components/app/chat'
@@ -73,6 +75,7 @@ const Main: FC<IMainProps> = ({
const [plan, setPlan] = useState<string>('basic') // basic/plus/pro
const [canReplaceLogo, setCanReplaceLogo] = useState<boolean>(false)
const [customConfig, setCustomConfig] = useState<any>(null)
const [appMeta, setAppMeta] = useState<AppMeta | null>(null)
// 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.
@@ -111,6 +114,7 @@ const Main: FC<IMainProps> = ({
existConversationInfo,
setExistConversationInfo,
} = useConversation()
const [suggestedQuestions, setSuggestQuestions] = useState<string[]>([])
const [hasMore, setHasMore] = useState<boolean>(true)
const [hasPinnedMore, setHasPinnedMore] = useState<boolean>(true)
const onMoreLoaded = ({ data: conversations, has_more }: any) => {
@@ -197,6 +201,14 @@ const Main: FC<IMainProps> = ({
const conversationIntroduction = currConversationInfo?.introduction || ''
const [controlChatUpdateAllConversation, setControlChatUpdateAllConversation] = useState(0)
// onData change thought (the produce obj). https://github.com/immerjs/immer/issues/576
useEffect(() => {
setAutoFreeze(false)
return () => {
setAutoFreeze(true)
}
}, [])
useEffect(() => {
(async () => {
if (controlChatUpdateAllConversation && !isNewConversation) {
@@ -251,14 +263,16 @@ const Main: FC<IMainProps> = ({
id: `question-${item.id}`,
content: item.query,
isAnswer: false,
message_files: item.message_files,
message_files: item.message_files?.filter((file: any) => file.belongs_to === 'user') || [],
})
newChatList.push({
id: item.id,
content: item.answer,
agent_thoughts: addFileInfos(item.agent_thoughts ? sortAgentSorts(item.agent_thoughts) : item.agent_thoughts, item.message_files),
feedback: item.feedback,
isAnswer: true,
citation: item.retriever_resources,
message_files: item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [],
})
})
setChatList(newChatList)
@@ -321,14 +335,13 @@ const Main: FC<IMainProps> = ({
const caculatedPromptVariables = inputs || currInputs || null
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: true,
suggestedQuestions: openingSuggestedQuestions,
}
if (caculatedIntroduction)
return [openstatement]
@@ -356,7 +369,7 @@ const Main: FC<IMainProps> = ({
},
plan: 'basic',
}
: fetchAppInfo(), fetchAllConversations(), fetchAppParams(isInstalledApp, installedAppInfo?.id)])
: fetchAppInfo(), fetchAllConversations(), fetchAppParams(isInstalledApp, installedAppInfo?.id), fetchAppMeta(isInstalledApp, installedAppInfo?.id)])
}
const { data: fileUploadConfigResponse } = useSWR(isInstalledApp ? { url: '/files/upload' } : null, fetchFileUploadConfig)
@@ -365,7 +378,8 @@ const Main: FC<IMainProps> = ({
useEffect(() => {
(async () => {
try {
const [appData, conversationData, appParams]: any = await fetchInitData()
const [appData, conversationData, appParams, appMeta]: any = await fetchInitData()
setAppMeta(appMeta)
const { app_id: appId, site: siteInfo, plan, can_replace_logo, custom_config }: any = appData
setAppId(appId)
setPlan(plan)
@@ -380,7 +394,7 @@ const Main: FC<IMainProps> = ({
const isNotNewConversation = allConversations.some(item => item.id === _conversationId)
setAllConversationList(allConversations)
// fetch new conversation info
const { user_input_form, opening_statement: introduction, suggested_questions_after_answer, speech_to_text, retriever_resource, file_upload, sensitive_word_avoidance }: any = appParams
const { user_input_form, opening_statement: introduction, suggested_questions, suggested_questions_after_answer, speech_to_text, retriever_resource, file_upload, sensitive_word_avoidance }: any = appParams
setVisionConfig({
...file_upload.image,
image_file_size_limit: appParams?.system_parameters?.image_file_size_limit,
@@ -393,6 +407,8 @@ const Main: FC<IMainProps> = ({
name: t('share.chat.newChatDefaultName'),
introduction,
})
setOpeningSuggestedQuestions(suggested_questions || [])
setSiteInfo(siteInfo as SiteInfo)
setPromptConfig({
prompt_template,
@@ -460,16 +476,41 @@ const Main: FC<IMainProps> = ({
const [controlFocus, setControlFocus] = useState(0)
const [isShowSuggestion, setIsShowSuggestion] = useState(false)
const doShowSuggestion = isShowSuggestion && !isResponsing
const [suggestQuestions, setSuggestQuestions] = useState<string[]>([])
const [openingSuggestedQuestions, setOpeningSuggestedQuestions] = useState<string[]>([])
const [messageTaskId, setMessageTaskId] = useState('')
const [hasStopResponded, setHasStopResponded, getHasStopResponded] = useGetState(false)
const [isResponsingConIsCurrCon, setIsResponsingConCurrCon, getIsResponsingConIsCurrCon] = useGetState(true)
const [userQuery, setUserQuery] = useState('')
const [visionConfig, setVisionConfig] = useState<VisionSettings>({
enabled: false,
number_limits: 2,
detail: Resolution.low,
transfer_methods: [TransferMethod.local_file],
})
const updateCurrentQA = ({
responseItem,
questionId,
placeholderAnswerId,
questionItem,
}: {
responseItem: IChatItem
questionId: string
placeholderAnswerId: string
questionItem: IChatItem
}) => {
// 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)
}
const handleSend = async (message: string, files?: VisionFile[]) => {
if (isResponsing) {
notify({ type: 'info', message: t('appDebug.errorMessage.waitForResponse') })
@@ -506,6 +547,7 @@ const Main: FC<IMainProps> = ({
content: message,
isAnswer: false,
message_files: files,
}
const placeholderAnswerId = `answer-placeholder-${Date.now()}`
@@ -518,12 +560,18 @@ const Main: FC<IMainProps> = ({
const newList = [...getChatList(), questionItem, placeholderAnswerItem]
setChatList(newList)
let isAgentMode = false
// answer
const responseItem: IChatItem = {
id: `${Date.now()}`,
content: '',
agent_thoughts: [],
message_files: [],
isAnswer: true,
}
let hasSetResponseId = false
const prevTempNewConversationId = getCurrConversationId() || '-1'
let tempNewConversationId = prevTempNewConversationId
@@ -536,8 +584,19 @@ const Main: FC<IMainProps> = ({
setAbortController(abortController)
},
onData: (message: string, isFirstMessage: boolean, { conversationId: newConversationId, messageId, taskId }: any) => {
responseItem.content = responseItem.content + message
responseItem.id = messageId
if (!isAgentMode) {
responseItem.content = responseItem.content + message
}
else {
const lastThought = responseItem.agent_thoughts?.[responseItem.agent_thoughts?.length - 1]
if (lastThought)
lastThought.thought = lastThought.thought + message // need immer setAutoFreeze
}
if (messageId && !hasSetResponseId) {
responseItem.id = messageId
hasSetResponseId = true
}
if (isFirstMessage && newConversationId)
tempNewConversationId = newConversationId
@@ -547,16 +606,12 @@ const Main: FC<IMainProps> = ({
setIsResponsingConCurrCon(false)
return
}
// 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)
updateCurrentQA({
responseItem,
questionId,
placeholderAnswerId,
questionItem,
})
},
async onCompleted(hasError?: boolean) {
if (hasError)
@@ -583,6 +638,56 @@ const Main: FC<IMainProps> = ({
}
setResponsingFalse()
},
onFile(file) {
const lastThought = responseItem.agent_thoughts?.[responseItem.agent_thoughts?.length - 1]
if (lastThought)
lastThought.message_files = [...(lastThought as any).message_files, { ...file }]
updateCurrentQA({
responseItem,
questionId,
placeholderAnswerId,
questionItem,
})
},
onThought(thought) {
// console.log(`${thought.id};${thought.thought};${thought.tool};${thought.tool_input}`)
isAgentMode = true
const response = responseItem as any
if (thought.message_id && !hasSetResponseId) {
response.id = thought.message_id
hasSetResponseId = true
}
// responseItem.id = thought.message_id;
if (response.agent_thoughts.length === 0) {
response.agent_thoughts.push(thought)
}
else {
const lastThought = response.agent_thoughts[response.agent_thoughts.length - 1]
// thought changed but still the same thought, so update.
if (lastThought.id === thought.id) {
thought.thought = lastThought.thought
thought.message_files = lastThought.message_files
responseItem.agent_thoughts![response.agent_thoughts.length - 1] = thought
}
else {
responseItem.agent_thoughts!.push(thought)
}
}
// has switched to other conversation
if (prevTempNewConversationId !== getCurrConversationId()) {
setIsResponsingConCurrCon(false)
return false
}
updateCurrentQA({
responseItem,
questionId,
placeholderAnswerId,
questionItem,
})
},
onMessageEnd: (messageEnd) => {
if (messageEnd.metadata?.annotation_reply) {
responseItem.id = messageEnd.id
@@ -765,6 +870,8 @@ const Main: FC<IMainProps> = ({
<div className='h-full overflow-y-auto' ref={chatListDomRef}>
<Chat
chatList={chatList}
query={userQuery}
onQueryChange={setUserQuery}
onSend={handleSend}
isHideFeedbackEdit
onFeedback={handleFeedback}
@@ -778,13 +885,14 @@ const Main: FC<IMainProps> = ({
checkCanSend={checkCanSend}
controlFocus={controlFocus}
isShowSuggestion={doShowSuggestion}
suggestionList={suggestQuestions}
suggestionList={suggestedQuestions}
isShowSpeechToText={speechToTextConfig?.enabled}
isShowCitation={citationConfig?.enabled && isInstalledApp}
visionConfig={{
...visionConfig,
image_file_size_limit: fileUploadConfigResponse ? fileUploadConfigResponse.image_file_size_limit : visionConfig.image_file_size_limit,
}}
allToolIcons={appMeta?.tool_icons || {}}
/>
</div>
</div>)