index.tsx 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823
  1. /* eslint-disable @typescript-eslint/no-use-before-define */
  2. 'use client'
  3. import type { FC } from 'react'
  4. import React, { useEffect, useRef, useState } from 'react'
  5. import cn from 'classnames'
  6. import { useTranslation } from 'react-i18next'
  7. import { useContext } from 'use-context-selector'
  8. import produce, { setAutoFreeze } from 'immer'
  9. import { useBoolean, useGetState } from 'ahooks'
  10. import { checkOrSetAccessToken } from '../utils'
  11. import AppUnavailable from '../../base/app-unavailable'
  12. import useConversation from './hooks/use-conversation'
  13. import { ToastContext } from '@/app/components/base/toast'
  14. import ConfigScene from '@/app/components/share/chatbot/config-scence'
  15. import Header from '@/app/components/share/header'
  16. import {
  17. fetchAppInfo,
  18. fetchAppMeta,
  19. fetchAppParams,
  20. fetchChatList,
  21. fetchConversations,
  22. fetchSuggestedQuestions,
  23. generationConversationName,
  24. sendChatMessage,
  25. stopChatMessageResponding,
  26. updateFeedback,
  27. } from '@/service/share'
  28. import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils'
  29. import type { AppMeta, ConversationItem, SiteInfo } from '@/models/share'
  30. import type { PromptConfig, SuggestedQuestionsAfterAnswerConfig } from '@/models/debug'
  31. import type { Feedbacktype, IChatItem } from '@/app/components/app/chat/type'
  32. import Chat from '@/app/components/app/chat'
  33. import { changeLanguage } from '@/i18n/i18next-config'
  34. import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
  35. import Loading from '@/app/components/base/loading'
  36. import { replaceStringWithValues } from '@/app/components/app/configuration/prompt-value-panel'
  37. import { userInputsFormToPromptVariables } from '@/utils/model-config'
  38. import type { InstalledApp } from '@/models/explore'
  39. import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback'
  40. import LogoHeader from '@/app/components/base/logo/logo-embeded-chat-header'
  41. import LogoAvatar from '@/app/components/base/logo/logo-embeded-chat-avatar'
  42. import type { VisionFile, VisionSettings } from '@/types/app'
  43. import { Resolution, TransferMethod } from '@/types/app'
  44. import type { Annotation as AnnotationType } from '@/models/log'
  45. export type IMainProps = {
  46. isInstalledApp?: boolean
  47. installedAppInfo?: InstalledApp
  48. }
  49. const Main: FC<IMainProps> = ({
  50. isInstalledApp = false,
  51. installedAppInfo,
  52. }) => {
  53. const { t } = useTranslation()
  54. const media = useBreakpoints()
  55. const isMobile = media === MediaType.mobile
  56. /*
  57. * app info
  58. */
  59. const [appUnavailable, setAppUnavailable] = useState<boolean>(false)
  60. const [isUnknwonReason, setIsUnknwonReason] = useState<boolean>(false)
  61. const [appId, setAppId] = useState<string>('')
  62. const [isPublicVersion, setIsPublicVersion] = useState<boolean>(true)
  63. const [siteInfo, setSiteInfo] = useState<SiteInfo | null>()
  64. const [promptConfig, setPromptConfig] = useState<PromptConfig | null>(null)
  65. const [inited, setInited] = useState<boolean>(false)
  66. const [plan, setPlan] = useState<string>('basic') // basic/plus/pro
  67. const [canReplaceLogo, setCanReplaceLogo] = useState<boolean>(false)
  68. const [customConfig, setCustomConfig] = useState<any>(null)
  69. const [appMeta, setAppMeta] = useState<AppMeta | null>(null)
  70. // Can Use metadata(https://beta.nextjs.org/docs/api-reference/metadata) to set title. But it only works in server side client.
  71. useEffect(() => {
  72. if (siteInfo?.title) {
  73. if (canReplaceLogo)
  74. document.title = `${siteInfo.title}`
  75. else
  76. document.title = `${siteInfo.title} - Powered by Dify`
  77. }
  78. }, [siteInfo?.title, canReplaceLogo])
  79. // onData change thought (the produce obj). https://github.com/immerjs/immer/issues/576
  80. useEffect(() => {
  81. setAutoFreeze(false)
  82. return () => {
  83. setAutoFreeze(true)
  84. }
  85. }, [])
  86. /*
  87. * conversation info
  88. */
  89. const [allConversationList, setAllConversationList] = useState<ConversationItem[]>([])
  90. const [isClearConversationList, { setTrue: clearConversationListTrue, setFalse: clearConversationListFalse }] = useBoolean(false)
  91. const [isClearPinnedConversationList, { setTrue: clearPinnedConversationListTrue, setFalse: clearPinnedConversationListFalse }] = useBoolean(false)
  92. const {
  93. conversationList,
  94. setConversationList,
  95. pinnedConversationList,
  96. setPinnedConversationList,
  97. currConversationId,
  98. getCurrConversationId,
  99. setCurrConversationId,
  100. getConversationIdFromStorage,
  101. isNewConversation,
  102. currConversationInfo,
  103. currInputs,
  104. newConversationInputs,
  105. // existConversationInputs,
  106. resetNewConversationInputs,
  107. setCurrInputs,
  108. setNewConversationInfo,
  109. setExistConversationInfo,
  110. } = useConversation()
  111. const [hasMore, setHasMore] = useState<boolean>(true)
  112. const [hasPinnedMore, setHasPinnedMore] = useState<boolean>(true)
  113. const onMoreLoaded = ({ data: conversations, has_more }: any) => {
  114. setHasMore(has_more)
  115. if (isClearConversationList) {
  116. setConversationList(conversations)
  117. clearConversationListFalse()
  118. }
  119. else {
  120. setConversationList([...conversationList, ...conversations])
  121. }
  122. }
  123. const onPinnedMoreLoaded = ({ data: conversations, has_more }: any) => {
  124. setHasPinnedMore(has_more)
  125. if (isClearPinnedConversationList) {
  126. setPinnedConversationList(conversations)
  127. clearPinnedConversationListFalse()
  128. }
  129. else {
  130. setPinnedConversationList([...pinnedConversationList, ...conversations])
  131. }
  132. }
  133. const [controlUpdateConversationList, setControlUpdateConversationList] = useState(0)
  134. const noticeUpdateList = () => {
  135. setHasMore(true)
  136. clearConversationListTrue()
  137. setHasPinnedMore(true)
  138. clearPinnedConversationListTrue()
  139. setControlUpdateConversationList(Date.now())
  140. }
  141. const [suggestedQuestionsAfterAnswerConfig, setSuggestedQuestionsAfterAnswerConfig] = useState<SuggestedQuestionsAfterAnswerConfig | null>(null)
  142. const [speechToTextConfig, setSpeechToTextConfig] = useState<SuggestedQuestionsAfterAnswerConfig | null>(null)
  143. const [textToSpeechConfig, setTextToSpeechConfig] = useState<SuggestedQuestionsAfterAnswerConfig | null>(null)
  144. const [citationConfig, setCitationConfig] = useState<SuggestedQuestionsAfterAnswerConfig | null>(null)
  145. const [conversationIdChangeBecauseOfNew, setConversationIdChangeBecauseOfNew, getConversationIdChangeBecauseOfNew] = useGetState(false)
  146. const [isChatStarted, { setTrue: setChatStarted, setFalse: setChatNotStarted }] = useBoolean(false)
  147. const handleStartChat = (inputs: Record<string, any>) => {
  148. createNewChat()
  149. setConversationIdChangeBecauseOfNew(true)
  150. setCurrInputs(inputs)
  151. setChatStarted()
  152. // parse variables in introduction
  153. setChatList(generateNewChatListWithOpenstatement('', inputs))
  154. }
  155. const hasSetInputs = (() => {
  156. if (!isNewConversation)
  157. return true
  158. return isChatStarted
  159. })()
  160. // const conversationName = currConversationInfo?.name || t('share.chat.newChatDefaultName') as string
  161. const conversationIntroduction = currConversationInfo?.introduction || ''
  162. const handleConversationSwitch = () => {
  163. if (!inited)
  164. return
  165. if (!appId) {
  166. // wait for appId
  167. setTimeout(handleConversationSwitch, 100)
  168. return
  169. }
  170. // update inputs of current conversation
  171. let notSyncToStateIntroduction = ''
  172. let notSyncToStateInputs: Record<string, any> | undefined | null = {}
  173. if (!isNewConversation) {
  174. const item = allConversationList.find(item => item.id === currConversationId)
  175. notSyncToStateInputs = item?.inputs || {}
  176. setCurrInputs(notSyncToStateInputs)
  177. notSyncToStateIntroduction = item?.introduction || ''
  178. setExistConversationInfo({
  179. name: item?.name || '',
  180. introduction: notSyncToStateIntroduction,
  181. })
  182. }
  183. else {
  184. notSyncToStateInputs = newConversationInputs
  185. setCurrInputs(notSyncToStateInputs)
  186. }
  187. // update chat list of current conversation
  188. if (!isNewConversation && !conversationIdChangeBecauseOfNew && !isResponding) {
  189. fetchChatList(currConversationId, isInstalledApp, installedAppInfo?.id).then((res: any) => {
  190. const { data } = res
  191. const newChatList: IChatItem[] = generateNewChatListWithOpenstatement(notSyncToStateIntroduction, notSyncToStateInputs)
  192. data.forEach((item: any) => {
  193. newChatList.push({
  194. id: `question-${item.id}`,
  195. content: item.query,
  196. isAnswer: false,
  197. message_files: item.message_files?.filter((file: any) => file.belongs_to === 'user') || [],
  198. })
  199. newChatList.push({
  200. id: item.id,
  201. content: item.answer,
  202. agent_thoughts: addFileInfos(item.agent_thoughts ? sortAgentSorts(item.agent_thoughts) : item.agent_thoughts, item.message_files),
  203. feedback: item.feedback,
  204. isAnswer: true,
  205. citation: item.retriever_resources,
  206. message_files: item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [],
  207. })
  208. })
  209. setChatList(newChatList)
  210. })
  211. }
  212. if (isNewConversation && isChatStarted)
  213. setChatList(generateNewChatListWithOpenstatement())
  214. setControlFocus(Date.now())
  215. }
  216. useEffect(handleConversationSwitch, [currConversationId, inited])
  217. /*
  218. * chat info. chat is under conversation.
  219. */
  220. const [chatList, setChatList, getChatList] = useGetState<IChatItem[]>([])
  221. const chatListDomRef = useRef<HTMLDivElement>(null)
  222. useEffect(() => {
  223. // scroll to bottom
  224. if (chatListDomRef.current)
  225. chatListDomRef.current.scrollTop = chatListDomRef.current.scrollHeight
  226. }, [chatList, currConversationId])
  227. // user can not edit inputs if user had send message
  228. const canEditInputs = !chatList.some(item => item.isAnswer === false) && isNewConversation
  229. const createNewChat = async () => {
  230. // if new chat is already exist, do not create new chat
  231. abortController?.abort()
  232. setRespondingFalse()
  233. if (conversationList.some(item => item.id === '-1'))
  234. return
  235. setConversationList(produce(conversationList, (draft) => {
  236. draft.unshift({
  237. id: '-1',
  238. name: t('share.chat.newChatDefaultName'),
  239. inputs: newConversationInputs,
  240. introduction: conversationIntroduction,
  241. })
  242. }))
  243. }
  244. // sometime introduction is not applied to state
  245. const generateNewChatListWithOpenstatement = (introduction?: string, inputs?: Record<string, any> | null) => {
  246. let caculatedIntroduction = introduction || conversationIntroduction || ''
  247. const caculatedPromptVariables = inputs || currInputs || null
  248. if (caculatedIntroduction && caculatedPromptVariables)
  249. caculatedIntroduction = replaceStringWithValues(caculatedIntroduction, promptConfig?.prompt_variables || [], caculatedPromptVariables)
  250. const openstatement = {
  251. id: `${Date.now()}`,
  252. content: caculatedIntroduction,
  253. isAnswer: true,
  254. feedbackDisabled: true,
  255. isOpeningStatement: isPublicVersion,
  256. }
  257. if (caculatedIntroduction)
  258. return [openstatement]
  259. return []
  260. }
  261. const fetchAllConversations = () => {
  262. return fetchConversations(isInstalledApp, installedAppInfo?.id, undefined, undefined, 100)
  263. }
  264. const fetchInitData = async () => {
  265. if (!isInstalledApp)
  266. await checkOrSetAccessToken()
  267. return Promise.all([isInstalledApp
  268. ? {
  269. app_id: installedAppInfo?.id,
  270. site: {
  271. title: installedAppInfo?.app.name,
  272. prompt_public: false,
  273. copyright: '',
  274. },
  275. plan: 'basic',
  276. }
  277. : fetchAppInfo(), fetchAllConversations(), fetchAppParams(isInstalledApp, installedAppInfo?.id), fetchAppMeta(isInstalledApp, installedAppInfo?.id)])
  278. }
  279. // init
  280. useEffect(() => {
  281. (async () => {
  282. try {
  283. const [appData, conversationData, appParams, appMeta]: any = await fetchInitData()
  284. setAppMeta(appMeta)
  285. const { app_id: appId, site: siteInfo, plan, can_replace_logo, custom_config }: any = appData
  286. setAppId(appId)
  287. setPlan(plan)
  288. setCanReplaceLogo(can_replace_logo)
  289. setCustomConfig(custom_config)
  290. const tempIsPublicVersion = siteInfo.prompt_public
  291. setIsPublicVersion(tempIsPublicVersion)
  292. const prompt_template = ''
  293. // handle current conversation id
  294. const { data: allConversations } = conversationData as { data: ConversationItem[]; has_more: boolean }
  295. const _conversationId = getConversationIdFromStorage(appId)
  296. const isNotNewConversation = allConversations.some(item => item.id === _conversationId)
  297. setAllConversationList(allConversations)
  298. // fetch new conversation info
  299. const { user_input_form, opening_statement: introduction, suggested_questions_after_answer, speech_to_text, text_to_speech, retriever_resource, file_upload, sensitive_word_avoidance }: any = appParams
  300. setVisionConfig({
  301. ...file_upload.image,
  302. image_file_size_limit: appParams?.system_parameters?.image_file_size_limit,
  303. })
  304. const prompt_variables = userInputsFormToPromptVariables(user_input_form)
  305. if (siteInfo.default_language)
  306. changeLanguage(siteInfo.default_language)
  307. setNewConversationInfo({
  308. name: t('share.chat.newChatDefaultName'),
  309. introduction,
  310. })
  311. setSiteInfo(siteInfo as SiteInfo)
  312. setPromptConfig({
  313. prompt_template,
  314. prompt_variables,
  315. } as PromptConfig)
  316. setSuggestedQuestionsAfterAnswerConfig(suggested_questions_after_answer)
  317. setSpeechToTextConfig(speech_to_text)
  318. setTextToSpeechConfig(text_to_speech)
  319. setCitationConfig(retriever_resource)
  320. // setConversationList(conversations as ConversationItem[])
  321. if (isNotNewConversation)
  322. setCurrConversationId(_conversationId, appId, false)
  323. setInited(true)
  324. }
  325. catch (e: any) {
  326. if (e.status === 404) {
  327. setAppUnavailable(true)
  328. }
  329. else {
  330. setIsUnknwonReason(true)
  331. setAppUnavailable(true)
  332. }
  333. }
  334. })()
  335. }, [])
  336. const [isResponding, { setTrue: setRespondingTrue, setFalse: setRespondingFalse }] = useBoolean(false)
  337. const [abortController, setAbortController] = useState<AbortController | null>(null)
  338. const { notify } = useContext(ToastContext)
  339. const logError = (message: string) => {
  340. notify({ type: 'error', message })
  341. }
  342. const checkCanSend = () => {
  343. if (currConversationId !== '-1')
  344. return true
  345. const prompt_variables = promptConfig?.prompt_variables
  346. const inputs = currInputs
  347. if (!inputs || !prompt_variables || prompt_variables?.length === 0)
  348. return true
  349. let hasEmptyInput = ''
  350. const requiredVars = prompt_variables?.filter(({ key, name, required }) => {
  351. const res = (!key || !key.trim()) || (!name || !name.trim()) || (required || required === undefined || required === null)
  352. return res
  353. }) || [] // compatible with old version
  354. requiredVars.forEach(({ key, name }) => {
  355. if (hasEmptyInput)
  356. return
  357. if (!inputs?.[key])
  358. hasEmptyInput = name
  359. })
  360. if (hasEmptyInput) {
  361. logError(t('appDebug.errorMessage.valueOfVarRequired', { key: hasEmptyInput }))
  362. return false
  363. }
  364. return !hasEmptyInput
  365. }
  366. const [controlFocus, setControlFocus] = useState(0)
  367. const [isShowSuggestion, setIsShowSuggestion] = useState(false)
  368. const doShowSuggestion = isShowSuggestion && !isResponding
  369. const [suggestQuestions, setSuggestQuestions] = useState<string[]>([])
  370. const [messageTaskId, setMessageTaskId] = useState('')
  371. const [hasStopResponded, setHasStopResponded, getHasStopResponded] = useGetState(false)
  372. const [isRespondingConIsCurrCon, setIsRespondingConCurrCon, getIsRespondingConIsCurrCon] = useGetState(true)
  373. const [shouldReload, setShouldReload] = useState(false)
  374. const [userQuery, setUserQuery] = useState('')
  375. const [visionConfig, setVisionConfig] = useState<VisionSettings>({
  376. enabled: false,
  377. number_limits: 2,
  378. detail: Resolution.low,
  379. transfer_methods: [TransferMethod.local_file],
  380. })
  381. const updateCurrentQA = ({
  382. responseItem,
  383. questionId,
  384. placeholderAnswerId,
  385. questionItem,
  386. }: {
  387. responseItem: IChatItem
  388. questionId: string
  389. placeholderAnswerId: string
  390. questionItem: IChatItem
  391. }) => {
  392. // closesure new list is outdated.
  393. const newListWithAnswer = produce(
  394. getChatList().filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId),
  395. (draft) => {
  396. if (!draft.find(item => item.id === questionId))
  397. draft.push({ ...questionItem })
  398. draft.push({ ...responseItem })
  399. })
  400. setChatList(newListWithAnswer)
  401. }
  402. const handleSend = async (message: string, files?: VisionFile[]) => {
  403. if (isResponding) {
  404. notify({ type: 'info', message: t('appDebug.errorMessage.waitForResponse') })
  405. return
  406. }
  407. if (files?.find(item => item.transfer_method === TransferMethod.local_file && !item.upload_file_id)) {
  408. notify({ type: 'info', message: t('appDebug.errorMessage.waitForImgUpload') })
  409. return false
  410. }
  411. const data: Record<string, any> = {
  412. inputs: currInputs,
  413. query: message,
  414. conversation_id: isNewConversation ? null : currConversationId,
  415. }
  416. if (visionConfig.enabled && files && files?.length > 0) {
  417. data.files = files.map((item) => {
  418. if (item.transfer_method === TransferMethod.local_file) {
  419. return {
  420. ...item,
  421. url: '',
  422. }
  423. }
  424. return item
  425. })
  426. }
  427. // qustion
  428. const questionId = `question-${Date.now()}`
  429. const questionItem = {
  430. id: questionId,
  431. content: message,
  432. isAnswer: false,
  433. message_files: files,
  434. }
  435. const placeholderAnswerId = `answer-placeholder-${Date.now()}`
  436. const placeholderAnswerItem = {
  437. id: placeholderAnswerId,
  438. content: '',
  439. isAnswer: true,
  440. }
  441. const newList = [...getChatList(), questionItem, placeholderAnswerItem]
  442. setChatList(newList)
  443. let isAgentMode = false
  444. // answer
  445. const responseItem: IChatItem = {
  446. id: `${Date.now()}`,
  447. content: '',
  448. agent_thoughts: [],
  449. message_files: [],
  450. isAnswer: true,
  451. }
  452. let hasSetResponseId = false
  453. const prevTempNewConversationId = getCurrConversationId() || '-1'
  454. let tempNewConversationId = prevTempNewConversationId
  455. setHasStopResponded(false)
  456. setRespondingTrue()
  457. setIsShowSuggestion(false)
  458. sendChatMessage(data, {
  459. getAbortController: (abortController) => {
  460. setAbortController(abortController)
  461. },
  462. onData: (message: string, isFirstMessage: boolean, { conversationId: newConversationId, messageId, taskId }: any) => {
  463. if (!isAgentMode) {
  464. responseItem.content = responseItem.content + message
  465. }
  466. else {
  467. const lastThought = responseItem.agent_thoughts?.[responseItem.agent_thoughts?.length - 1]
  468. if (lastThought)
  469. lastThought.thought = lastThought.thought + message // need immer setAutoFreeze
  470. }
  471. if (messageId && !hasSetResponseId) {
  472. responseItem.id = messageId
  473. hasSetResponseId = true
  474. }
  475. if (isFirstMessage && newConversationId)
  476. tempNewConversationId = newConversationId
  477. setMessageTaskId(taskId)
  478. // has switched to other conversation
  479. if (prevTempNewConversationId !== getCurrConversationId()) {
  480. setIsRespondingConCurrCon(false)
  481. return
  482. }
  483. updateCurrentQA({
  484. responseItem,
  485. questionId,
  486. placeholderAnswerId,
  487. questionItem,
  488. })
  489. },
  490. async onCompleted(hasError?: boolean) {
  491. if (hasError)
  492. return
  493. if (getConversationIdChangeBecauseOfNew()) {
  494. const { data: allConversations }: any = await fetchAllConversations()
  495. const newItem: any = await generationConversationName(isInstalledApp, installedAppInfo?.id, allConversations[0].id)
  496. const newAllConversations = produce(allConversations, (draft: any) => {
  497. draft[0].name = newItem.name
  498. })
  499. setAllConversationList(newAllConversations as any)
  500. noticeUpdateList()
  501. }
  502. setConversationIdChangeBecauseOfNew(false)
  503. resetNewConversationInputs()
  504. setChatNotStarted()
  505. setCurrConversationId(tempNewConversationId, appId, true)
  506. if (suggestedQuestionsAfterAnswerConfig?.enabled && !getHasStopResponded()) {
  507. const { data }: any = await fetchSuggestedQuestions(responseItem.id, isInstalledApp, installedAppInfo?.id)
  508. setSuggestQuestions(data)
  509. setIsShowSuggestion(true)
  510. }
  511. setRespondingFalse()
  512. },
  513. onFile(file) {
  514. const lastThought = responseItem.agent_thoughts?.[responseItem.agent_thoughts?.length - 1]
  515. if (lastThought)
  516. lastThought.message_files = [...(lastThought as any).message_files, { ...file }]
  517. updateCurrentQA({
  518. responseItem,
  519. questionId,
  520. placeholderAnswerId,
  521. questionItem,
  522. })
  523. },
  524. onThought(thought) {
  525. isAgentMode = true
  526. const response = responseItem as any
  527. if (thought.message_id && !hasSetResponseId) {
  528. response.id = thought.message_id
  529. hasSetResponseId = true
  530. }
  531. // responseItem.id = thought.message_id;
  532. if (response.agent_thoughts.length === 0) {
  533. response.agent_thoughts.push(thought)
  534. }
  535. else {
  536. const lastThought = response.agent_thoughts[response.agent_thoughts.length - 1]
  537. // thought changed but still the same thought, so update.
  538. if (lastThought.id === thought.id) {
  539. thought.thought = lastThought.thought
  540. thought.message_files = lastThought.message_files
  541. responseItem.agent_thoughts![response.agent_thoughts.length - 1] = thought
  542. }
  543. else {
  544. responseItem.agent_thoughts!.push(thought)
  545. }
  546. }
  547. // has switched to other conversation
  548. if (prevTempNewConversationId !== getCurrConversationId()) {
  549. setIsRespondingConCurrCon(false)
  550. return false
  551. }
  552. updateCurrentQA({
  553. responseItem,
  554. questionId,
  555. placeholderAnswerId,
  556. questionItem,
  557. })
  558. },
  559. onMessageEnd: (messageEnd) => {
  560. if (messageEnd.metadata?.annotation_reply) {
  561. responseItem.id = messageEnd.id
  562. responseItem.annotation = ({
  563. id: messageEnd.metadata.annotation_reply.id,
  564. authorName: messageEnd.metadata.annotation_reply.account.name,
  565. } as AnnotationType)
  566. const newListWithAnswer = produce(
  567. getChatList().filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId),
  568. (draft) => {
  569. if (!draft.find(item => item.id === questionId))
  570. draft.push({ ...questionItem })
  571. draft.push({
  572. ...responseItem,
  573. })
  574. })
  575. setChatList(newListWithAnswer)
  576. return
  577. }
  578. // not support show citation
  579. // responseItem.citation = messageEnd.retriever_resources
  580. if (!isInstalledApp)
  581. return
  582. const newListWithAnswer = produce(
  583. getChatList().filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId),
  584. (draft) => {
  585. if (!draft.find(item => item.id === questionId))
  586. draft.push({ ...questionItem })
  587. draft.push({ ...responseItem })
  588. })
  589. setChatList(newListWithAnswer)
  590. },
  591. onMessageReplace: (messageReplace) => {
  592. if (isInstalledApp) {
  593. responseItem.content = messageReplace.answer
  594. }
  595. else {
  596. setChatList(produce(
  597. getChatList(),
  598. (draft) => {
  599. const current = draft.find(item => item.id === messageReplace.id)
  600. if (current)
  601. current.content = messageReplace.answer
  602. },
  603. ))
  604. }
  605. },
  606. onError() {
  607. setRespondingFalse()
  608. // role back placeholder answer
  609. setChatList(produce(getChatList(), (draft) => {
  610. draft.splice(draft.findIndex(item => item.id === placeholderAnswerId), 1)
  611. }))
  612. },
  613. }, isInstalledApp, installedAppInfo?.id)
  614. }
  615. const handleFeedback = async (messageId: string, feedback: Feedbacktype) => {
  616. await updateFeedback({ url: `/messages/${messageId}/feedbacks`, body: { rating: feedback.rating } }, isInstalledApp, installedAppInfo?.id)
  617. const newChatList = chatList.map((item) => {
  618. if (item.id === messageId) {
  619. return {
  620. ...item,
  621. feedback,
  622. }
  623. }
  624. return item
  625. })
  626. setChatList(newChatList)
  627. notify({ type: 'success', message: t('common.api.success') })
  628. }
  629. const handleReload = () => {
  630. setCurrConversationId('-1', appId, false)
  631. setChatNotStarted()
  632. setShouldReload(false)
  633. createNewChat()
  634. }
  635. const handleConversationIdChange = (id: string) => {
  636. if (id === '-1') {
  637. createNewChat()
  638. setConversationIdChangeBecauseOfNew(true)
  639. }
  640. else {
  641. setConversationIdChangeBecauseOfNew(false)
  642. }
  643. // trigger handleConversationSwitch
  644. setCurrConversationId(id, appId)
  645. setIsShowSuggestion(false)
  646. }
  647. const difyIcon = (
  648. <LogoHeader />
  649. )
  650. if (appUnavailable)
  651. return <AppUnavailable isUnknwonReason={isUnknwonReason} />
  652. if (!appId || !siteInfo || !promptConfig) {
  653. return <div className='flex h-screen w-full'>
  654. <Loading type='app' />
  655. </div>
  656. }
  657. return (
  658. <div>
  659. <Header
  660. title={siteInfo.title}
  661. icon=''
  662. customerIcon={difyIcon}
  663. icon_background={siteInfo.icon_background}
  664. isEmbedScene={true}
  665. isMobile={isMobile}
  666. onCreateNewChat={() => handleConversationIdChange('-1')}
  667. />
  668. <div className={'flex bg-white overflow-hidden'}>
  669. <div className={cn(
  670. isInstalledApp ? 'h-full' : 'h-[calc(100vh_-_3rem)]',
  671. 'flex-grow flex flex-col overflow-y-auto',
  672. )
  673. }>
  674. <ConfigScene
  675. // conversationName={conversationName}
  676. hasSetInputs={hasSetInputs}
  677. isPublicVersion={isPublicVersion}
  678. siteInfo={siteInfo}
  679. promptConfig={promptConfig}
  680. onStartChat={handleStartChat}
  681. canEditInputs={canEditInputs}
  682. savedInputs={currInputs as Record<string, any>}
  683. onInputsChange={setCurrInputs}
  684. plan={plan}
  685. canReplaceLogo={canReplaceLogo}
  686. customConfig={customConfig}
  687. ></ConfigScene>
  688. {
  689. shouldReload && (
  690. <div className='flex items-center justify-between mb-5 px-4 py-2 bg-[#FEF0C7]'>
  691. <div className='flex items-center text-xs font-medium text-[#DC6803]'>
  692. <AlertTriangle className='mr-2 w-4 h-4' />
  693. {t('share.chat.temporarySystemIssue')}
  694. </div>
  695. <div
  696. className='flex items-center px-3 h-7 bg-white shadow-xs rounded-md text-xs font-medium text-gray-700 cursor-pointer'
  697. onClick={handleReload}
  698. >
  699. {t('share.chat.tryToSolve')}
  700. </div>
  701. </div>
  702. )
  703. }
  704. {
  705. hasSetInputs && (
  706. <div className={cn(doShowSuggestion ? 'pb-[140px]' : (isResponding ? 'pb-[113px]' : 'pb-[76px]'), 'relative grow h-[200px] pc:w-[794px] max-w-full mobile:w-full mx-auto mb-3.5 overflow-hidden')}>
  707. <div className='h-full overflow-y-auto' ref={chatListDomRef}>
  708. <Chat
  709. chatList={chatList}
  710. query={userQuery}
  711. onQueryChange={setUserQuery}
  712. onSend={handleSend}
  713. isHideFeedbackEdit
  714. onFeedback={handleFeedback}
  715. isResponding={isResponding}
  716. canStopResponding={!!messageTaskId && isRespondingConIsCurrCon}
  717. abortResponding={async () => {
  718. await stopChatMessageResponding(appId, messageTaskId, isInstalledApp, installedAppInfo?.id)
  719. setHasStopResponded(true)
  720. setRespondingFalse()
  721. }}
  722. checkCanSend={checkCanSend}
  723. controlFocus={controlFocus}
  724. isShowSuggestion={doShowSuggestion}
  725. suggestionList={suggestQuestions}
  726. displayScene='web'
  727. isShowSpeechToText={speechToTextConfig?.enabled}
  728. isShowTextToSpeech={textToSpeechConfig?.enabled}
  729. isShowCitation={citationConfig?.enabled && isInstalledApp}
  730. answerIcon={<LogoAvatar className='relative shrink-0' />}
  731. visionConfig={visionConfig}
  732. allToolIcons={appMeta?.tool_icons || {}}
  733. />
  734. </div>
  735. </div>)
  736. }
  737. {/* {isShowConfirm && (
  738. <Confirm
  739. title={t('share.chat.deleteConversation.title')}
  740. content={t('share.chat.deleteConversation.content')}
  741. isShow={isShowConfirm}
  742. onClose={hideConfirm}
  743. onConfirm={didDelete}
  744. onCancel={hideConfirm}
  745. />
  746. )} */}
  747. </div>
  748. </div>
  749. </div>
  750. )
  751. }
  752. export default React.memo(Main)