hooks.ts 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641
  1. import {
  2. useCallback,
  3. useEffect,
  4. useRef,
  5. useState,
  6. } from 'react'
  7. import { useTranslation } from 'react-i18next'
  8. import { produce, setAutoFreeze } from 'immer'
  9. import { uniqBy } from 'lodash-es'
  10. import { useParams, usePathname } from 'next/navigation'
  11. import { v4 as uuidV4 } from 'uuid'
  12. import type {
  13. ChatConfig,
  14. ChatItem,
  15. Inputs,
  16. } from '../types'
  17. import type { InputForm } from './type'
  18. import {
  19. getProcessedInputs,
  20. processOpeningStatement,
  21. } from './utils'
  22. import { TransferMethod } from '@/types/app'
  23. import { useToastContext } from '@/app/components/base/toast'
  24. import { ssePost } from '@/service/base'
  25. import type { Annotation } from '@/models/log'
  26. import { WorkflowRunningStatus } from '@/app/components/workflow/types'
  27. import useTimestamp from '@/hooks/use-timestamp'
  28. import { AudioPlayerManager } from '@/app/components/base/audio-btn/audio.player.manager'
  29. import type { FileEntity } from '@/app/components/base/file-uploader/types'
  30. import {
  31. getProcessedFiles,
  32. getProcessedFilesFromResponse,
  33. } from '@/app/components/base/file-uploader/utils'
  34. type GetAbortController = (abortController: AbortController) => void
  35. type SendCallback = {
  36. onGetConversationMessages?: (conversationId: string, getAbortController: GetAbortController) => Promise<any>
  37. onGetSuggestedQuestions?: (responseItemId: string, getAbortController: GetAbortController) => Promise<any>
  38. onConversationComplete?: (conversationId: string) => void
  39. isPublicAPI?: boolean
  40. }
  41. export const useChat = (
  42. config?: ChatConfig,
  43. formSettings?: {
  44. inputs: Inputs
  45. inputsForm: InputForm[]
  46. },
  47. prevChatList?: ChatItem[],
  48. stopChat?: (taskId: string) => void,
  49. ) => {
  50. const { t } = useTranslation()
  51. const { formatTime } = useTimestamp()
  52. const { notify } = useToastContext()
  53. const conversationId = useRef('')
  54. const hasStopResponded = useRef(false)
  55. const [isResponding, setIsResponding] = useState(false)
  56. const isRespondingRef = useRef(false)
  57. const [chatList, setChatList] = useState<ChatItem[]>(prevChatList || [])
  58. const chatListRef = useRef<ChatItem[]>(prevChatList || [])
  59. const taskIdRef = useRef('')
  60. const [suggestedQuestions, setSuggestQuestions] = useState<string[]>([])
  61. const conversationMessagesAbortControllerRef = useRef<AbortController | null>(null)
  62. const suggestedQuestionsAbortControllerRef = useRef<AbortController | null>(null)
  63. const params = useParams()
  64. const pathname = usePathname()
  65. useEffect(() => {
  66. setAutoFreeze(false)
  67. return () => {
  68. setAutoFreeze(true)
  69. }
  70. }, [])
  71. const handleUpdateChatList = useCallback((newChatList: ChatItem[]) => {
  72. setChatList(newChatList)
  73. chatListRef.current = newChatList
  74. }, [])
  75. const handleResponding = useCallback((isResponding: boolean) => {
  76. setIsResponding(isResponding)
  77. isRespondingRef.current = isResponding
  78. }, [])
  79. const getIntroduction = useCallback((str: string) => {
  80. return processOpeningStatement(str, formSettings?.inputs || {}, formSettings?.inputsForm || [])
  81. }, [formSettings?.inputs, formSettings?.inputsForm])
  82. useEffect(() => {
  83. if (config?.opening_statement) {
  84. handleUpdateChatList(produce(chatListRef.current, (draft) => {
  85. const index = draft.findIndex(item => item.isOpeningStatement)
  86. if (index > -1) {
  87. draft[index] = {
  88. ...draft[index],
  89. content: getIntroduction(config.opening_statement),
  90. suggestedQuestions: config.suggested_questions,
  91. }
  92. }
  93. else {
  94. draft.unshift({
  95. id: `${Date.now()}`,
  96. content: getIntroduction(config.opening_statement),
  97. isAnswer: true,
  98. isOpeningStatement: true,
  99. suggestedQuestions: config.suggested_questions,
  100. })
  101. }
  102. }))
  103. }
  104. }, [config?.opening_statement, getIntroduction, config?.suggested_questions, handleUpdateChatList])
  105. const handleStop = useCallback(() => {
  106. hasStopResponded.current = true
  107. handleResponding(false)
  108. if (stopChat && taskIdRef.current)
  109. stopChat(taskIdRef.current)
  110. if (conversationMessagesAbortControllerRef.current)
  111. conversationMessagesAbortControllerRef.current.abort()
  112. if (suggestedQuestionsAbortControllerRef.current)
  113. suggestedQuestionsAbortControllerRef.current.abort()
  114. }, [stopChat, handleResponding])
  115. const handleRestart = useCallback(() => {
  116. conversationId.current = ''
  117. taskIdRef.current = ''
  118. handleStop()
  119. const newChatList = config?.opening_statement
  120. ? [{
  121. id: `${Date.now()}`,
  122. content: config.opening_statement,
  123. isAnswer: true,
  124. isOpeningStatement: true,
  125. suggestedQuestions: config.suggested_questions,
  126. }]
  127. : []
  128. handleUpdateChatList(newChatList)
  129. setSuggestQuestions([])
  130. }, [
  131. config,
  132. handleStop,
  133. handleUpdateChatList,
  134. ])
  135. const updateCurrentQA = useCallback(({
  136. responseItem,
  137. questionId,
  138. placeholderAnswerId,
  139. questionItem,
  140. }: {
  141. responseItem: ChatItem
  142. questionId: string
  143. placeholderAnswerId: string
  144. questionItem: ChatItem
  145. }) => {
  146. const newListWithAnswer = produce(
  147. chatListRef.current.filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId),
  148. (draft) => {
  149. if (!draft.find(item => item.id === questionId))
  150. draft.push({ ...questionItem })
  151. draft.push({ ...responseItem })
  152. })
  153. handleUpdateChatList(newListWithAnswer)
  154. }, [handleUpdateChatList])
  155. const handleSend = useCallback(async (
  156. url: string,
  157. data: {
  158. query: string
  159. files?: FileEntity[]
  160. [key: string]: any
  161. },
  162. {
  163. onGetConversationMessages,
  164. onGetSuggestedQuestions,
  165. onConversationComplete,
  166. isPublicAPI,
  167. }: SendCallback,
  168. ) => {
  169. setSuggestQuestions([])
  170. if (isRespondingRef.current) {
  171. notify({ type: 'info', message: t('appDebug.errorMessage.waitForResponse') })
  172. return false
  173. }
  174. const questionId = `question-${Date.now()}`
  175. const questionItem = {
  176. id: questionId,
  177. content: data.query,
  178. isAnswer: false,
  179. message_files: data.files,
  180. }
  181. const placeholderAnswerId = `answer-placeholder-${Date.now()}`
  182. const placeholderAnswerItem = {
  183. id: placeholderAnswerId,
  184. content: '',
  185. isAnswer: true,
  186. }
  187. const newList = [...chatListRef.current, questionItem, placeholderAnswerItem]
  188. handleUpdateChatList(newList)
  189. // answer
  190. const responseItem: ChatItem = {
  191. id: placeholderAnswerId,
  192. content: '',
  193. agent_thoughts: [],
  194. message_files: [],
  195. isAnswer: true,
  196. }
  197. handleResponding(true)
  198. hasStopResponded.current = false
  199. const { query, files, inputs, ...restData } = data
  200. const bodyParams = {
  201. response_mode: 'streaming',
  202. conversation_id: conversationId.current,
  203. files: getProcessedFiles(files || []),
  204. query,
  205. inputs: getProcessedInputs(inputs || {}, formSettings?.inputsForm || []),
  206. ...restData,
  207. }
  208. if (bodyParams?.files?.length) {
  209. bodyParams.files = bodyParams.files.map((item) => {
  210. if (item.transfer_method === TransferMethod.local_file) {
  211. return {
  212. ...item,
  213. url: '',
  214. }
  215. }
  216. return item
  217. })
  218. }
  219. let isAgentMode = false
  220. let hasSetResponseId = false
  221. let ttsUrl = ''
  222. let ttsIsPublic = false
  223. if (params.token) {
  224. ttsUrl = '/text-to-audio'
  225. ttsIsPublic = true
  226. }
  227. else if (params.appId) {
  228. if (pathname.search('explore/installed') > -1)
  229. ttsUrl = `/installed-apps/${params.appId}/text-to-audio`
  230. else
  231. ttsUrl = `/apps/${params.appId}/text-to-audio`
  232. }
  233. const player = AudioPlayerManager.getInstance().getAudioPlayer(ttsUrl, ttsIsPublic, uuidV4(), 'none', 'none', (_: any): any => {})
  234. ssePost(
  235. url,
  236. {
  237. body: bodyParams,
  238. },
  239. {
  240. isPublicAPI,
  241. onData: (message: string, isFirstMessage: boolean, { conversationId: newConversationId, messageId, taskId }: any) => {
  242. if (!isAgentMode) {
  243. responseItem.content = responseItem.content + message
  244. }
  245. else {
  246. const lastThought = responseItem.agent_thoughts?.[responseItem.agent_thoughts?.length - 1]
  247. if (lastThought)
  248. lastThought.thought = lastThought.thought + message // need immer setAutoFreeze
  249. }
  250. if (messageId && !hasSetResponseId) {
  251. responseItem.id = messageId
  252. hasSetResponseId = true
  253. }
  254. if (isFirstMessage && newConversationId)
  255. conversationId.current = newConversationId
  256. taskIdRef.current = taskId
  257. if (messageId)
  258. responseItem.id = messageId
  259. updateCurrentQA({
  260. responseItem,
  261. questionId,
  262. placeholderAnswerId,
  263. questionItem,
  264. })
  265. },
  266. async onCompleted(hasError?: boolean) {
  267. handleResponding(false)
  268. if (hasError)
  269. return
  270. if (onConversationComplete)
  271. onConversationComplete(conversationId.current)
  272. if (conversationId.current && !hasStopResponded.current && onGetConversationMessages) {
  273. const { data }: any = await onGetConversationMessages(
  274. conversationId.current,
  275. newAbortController => conversationMessagesAbortControllerRef.current = newAbortController,
  276. )
  277. const newResponseItem = data.find((item: any) => item.id === responseItem.id)
  278. if (!newResponseItem)
  279. return
  280. const newChatList = produce(chatListRef.current, (draft) => {
  281. const index = draft.findIndex(item => item.id === responseItem.id)
  282. if (index !== -1) {
  283. const question = draft[index - 1]
  284. draft[index - 1] = {
  285. ...question,
  286. }
  287. draft[index] = {
  288. ...draft[index],
  289. content: newResponseItem.answer,
  290. log: [
  291. ...newResponseItem.message,
  292. ...(newResponseItem.message[newResponseItem.message.length - 1].role !== 'assistant'
  293. ? [
  294. {
  295. role: 'assistant',
  296. text: newResponseItem.answer,
  297. files: newResponseItem.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [],
  298. },
  299. ]
  300. : []),
  301. ],
  302. more: {
  303. time: formatTime(newResponseItem.created_at, 'hh:mm A'),
  304. tokens: newResponseItem.answer_tokens + newResponseItem.message_tokens,
  305. latency: newResponseItem.provider_response_latency.toFixed(2),
  306. },
  307. // for agent log
  308. conversationId: conversationId.current,
  309. input: {
  310. inputs: newResponseItem.inputs,
  311. query: newResponseItem.query,
  312. },
  313. }
  314. }
  315. })
  316. handleUpdateChatList(newChatList)
  317. }
  318. if (config?.suggested_questions_after_answer?.enabled && !hasStopResponded.current && onGetSuggestedQuestions) {
  319. try {
  320. const { data }: any = await onGetSuggestedQuestions(
  321. responseItem.id,
  322. newAbortController => suggestedQuestionsAbortControllerRef.current = newAbortController,
  323. )
  324. setSuggestQuestions(data)
  325. }
  326. catch (e) {
  327. setSuggestQuestions([])
  328. }
  329. }
  330. },
  331. onFile(file) {
  332. const lastThought = responseItem.agent_thoughts?.[responseItem.agent_thoughts?.length - 1]
  333. if (lastThought)
  334. responseItem.agent_thoughts![responseItem.agent_thoughts!.length - 1].message_files = [...(lastThought as any).message_files, file]
  335. updateCurrentQA({
  336. responseItem,
  337. questionId,
  338. placeholderAnswerId,
  339. questionItem,
  340. })
  341. },
  342. onThought(thought) {
  343. isAgentMode = true
  344. const response = responseItem as any
  345. if (thought.message_id && !hasSetResponseId)
  346. response.id = thought.message_id
  347. if (response.agent_thoughts.length === 0) {
  348. response.agent_thoughts.push(thought)
  349. }
  350. else {
  351. const lastThought = response.agent_thoughts[response.agent_thoughts.length - 1]
  352. // thought changed but still the same thought, so update.
  353. if (lastThought.id === thought.id) {
  354. thought.thought = lastThought.thought
  355. thought.message_files = lastThought.message_files
  356. responseItem.agent_thoughts![response.agent_thoughts.length - 1] = thought
  357. }
  358. else {
  359. responseItem.agent_thoughts!.push(thought)
  360. }
  361. }
  362. updateCurrentQA({
  363. responseItem,
  364. questionId,
  365. placeholderAnswerId,
  366. questionItem,
  367. })
  368. },
  369. onMessageEnd: (messageEnd) => {
  370. if (messageEnd.metadata?.annotation_reply) {
  371. responseItem.id = messageEnd.id
  372. responseItem.annotation = ({
  373. id: messageEnd.metadata.annotation_reply.id,
  374. authorName: messageEnd.metadata.annotation_reply.account.name,
  375. })
  376. const baseState = chatListRef.current.filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId)
  377. const newListWithAnswer = produce(
  378. baseState,
  379. (draft) => {
  380. if (!draft.find(item => item.id === questionId))
  381. draft.push({ ...questionItem })
  382. draft.push({
  383. ...responseItem,
  384. })
  385. })
  386. handleUpdateChatList(newListWithAnswer)
  387. return
  388. }
  389. responseItem.citation = messageEnd.metadata?.retriever_resources || []
  390. const processedFilesFromResponse = getProcessedFilesFromResponse(messageEnd.files || [])
  391. responseItem.allFiles = uniqBy([...(responseItem.allFiles || []), ...(processedFilesFromResponse || [])], 'id')
  392. const newListWithAnswer = produce(
  393. chatListRef.current.filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId),
  394. (draft) => {
  395. if (!draft.find(item => item.id === questionId))
  396. draft.push({ ...questionItem })
  397. draft.push({ ...responseItem })
  398. })
  399. handleUpdateChatList(newListWithAnswer)
  400. },
  401. onMessageReplace: (messageReplace) => {
  402. responseItem.content = messageReplace.answer
  403. },
  404. onError() {
  405. handleResponding(false)
  406. const newChatList = produce(chatListRef.current, (draft) => {
  407. draft.splice(draft.findIndex(item => item.id === placeholderAnswerId), 1)
  408. })
  409. handleUpdateChatList(newChatList)
  410. },
  411. onWorkflowStarted: ({ workflow_run_id, task_id }) => {
  412. taskIdRef.current = task_id
  413. responseItem.workflow_run_id = workflow_run_id
  414. responseItem.workflowProcess = {
  415. status: WorkflowRunningStatus.Running,
  416. tracing: [],
  417. }
  418. handleUpdateChatList(produce(chatListRef.current, (draft) => {
  419. const currentIndex = draft.findIndex(item => item.id === responseItem.id)
  420. draft[currentIndex] = {
  421. ...draft[currentIndex],
  422. ...responseItem,
  423. }
  424. }))
  425. },
  426. onWorkflowFinished: ({ data }) => {
  427. responseItem.workflowProcess!.status = data.status as WorkflowRunningStatus
  428. handleUpdateChatList(produce(chatListRef.current, (draft) => {
  429. const currentIndex = draft.findIndex(item => item.id === responseItem.id)
  430. draft[currentIndex] = {
  431. ...draft[currentIndex],
  432. ...responseItem,
  433. }
  434. }))
  435. },
  436. onIterationStart: ({ data }) => {
  437. responseItem.workflowProcess!.tracing!.push({
  438. ...data,
  439. status: WorkflowRunningStatus.Running,
  440. } as any)
  441. handleUpdateChatList(produce(chatListRef.current, (draft) => {
  442. const currentIndex = draft.findIndex(item => item.id === responseItem.id)
  443. draft[currentIndex] = {
  444. ...draft[currentIndex],
  445. ...responseItem,
  446. }
  447. }))
  448. },
  449. onIterationFinish: ({ data }) => {
  450. const tracing = responseItem.workflowProcess!.tracing!
  451. const iterationIndex = tracing.findIndex(item => item.node_id === data.node_id
  452. && (item.execution_metadata?.parallel_id === data.execution_metadata?.parallel_id || item.parallel_id === data.execution_metadata?.parallel_id))!
  453. tracing[iterationIndex] = {
  454. ...tracing[iterationIndex],
  455. ...data,
  456. status: WorkflowRunningStatus.Succeeded,
  457. } as any
  458. handleUpdateChatList(produce(chatListRef.current, (draft) => {
  459. const currentIndex = draft.findIndex(item => item.id === responseItem.id)
  460. draft[currentIndex] = {
  461. ...draft[currentIndex],
  462. ...responseItem,
  463. }
  464. }))
  465. },
  466. onNodeStarted: ({ data }) => {
  467. if (data.iteration_id)
  468. return
  469. responseItem.workflowProcess!.tracing!.push({
  470. ...data,
  471. status: WorkflowRunningStatus.Running,
  472. } as any)
  473. handleUpdateChatList(produce(chatListRef.current, (draft) => {
  474. const currentIndex = draft.findIndex(item => item.id === responseItem.id)
  475. draft[currentIndex] = {
  476. ...draft[currentIndex],
  477. ...responseItem,
  478. }
  479. }))
  480. },
  481. onNodeFinished: ({ data }) => {
  482. if (data.iteration_id)
  483. return
  484. const currentIndex = responseItem.workflowProcess!.tracing!.findIndex((item) => {
  485. if (!item.execution_metadata?.parallel_id)
  486. return item.node_id === data.node_id
  487. return item.node_id === data.node_id && (item.execution_metadata?.parallel_id === data.execution_metadata.parallel_id)
  488. })
  489. responseItem.workflowProcess!.tracing[currentIndex] = data as any
  490. handleUpdateChatList(produce(chatListRef.current, (draft) => {
  491. const currentIndex = draft.findIndex(item => item.id === responseItem.id)
  492. draft[currentIndex] = {
  493. ...draft[currentIndex],
  494. ...responseItem,
  495. }
  496. }))
  497. },
  498. onTTSChunk: (messageId: string, audio: string) => {
  499. if (!audio || audio === '')
  500. return
  501. player.playAudioWithAudio(audio, true)
  502. AudioPlayerManager.getInstance().resetMsgId(messageId)
  503. },
  504. onTTSEnd: (messageId: string, audio: string) => {
  505. player.playAudioWithAudio(audio, false)
  506. },
  507. })
  508. return true
  509. }, [
  510. config?.suggested_questions_after_answer,
  511. updateCurrentQA,
  512. t,
  513. notify,
  514. handleUpdateChatList,
  515. handleResponding,
  516. formatTime,
  517. params.token,
  518. params.appId,
  519. pathname,
  520. formSettings,
  521. ])
  522. const handleAnnotationEdited = useCallback((query: string, answer: string, index: number) => {
  523. handleUpdateChatList(chatListRef.current.map((item, i) => {
  524. if (i === index - 1) {
  525. return {
  526. ...item,
  527. content: query,
  528. }
  529. }
  530. if (i === index) {
  531. return {
  532. ...item,
  533. content: answer,
  534. annotation: {
  535. ...item.annotation,
  536. logAnnotation: undefined,
  537. } as any,
  538. }
  539. }
  540. return item
  541. }))
  542. }, [handleUpdateChatList])
  543. const handleAnnotationAdded = useCallback((annotationId: string, authorName: string, query: string, answer: string, index: number) => {
  544. handleUpdateChatList(chatListRef.current.map((item, i) => {
  545. if (i === index - 1) {
  546. return {
  547. ...item,
  548. content: query,
  549. }
  550. }
  551. if (i === index) {
  552. const answerItem = {
  553. ...item,
  554. content: item.content,
  555. annotation: {
  556. id: annotationId,
  557. authorName,
  558. logAnnotation: {
  559. content: answer,
  560. account: {
  561. id: '',
  562. name: authorName,
  563. email: '',
  564. },
  565. },
  566. } as Annotation,
  567. }
  568. return answerItem
  569. }
  570. return item
  571. }))
  572. }, [handleUpdateChatList])
  573. const handleAnnotationRemoved = useCallback((index: number) => {
  574. handleUpdateChatList(chatListRef.current.map((item, i) => {
  575. if (i === index) {
  576. return {
  577. ...item,
  578. content: item.content,
  579. annotation: {
  580. ...(item.annotation || {}),
  581. id: '',
  582. } as Annotation,
  583. }
  584. }
  585. return item
  586. }))
  587. }, [handleUpdateChatList])
  588. return {
  589. chatList,
  590. chatListRef,
  591. handleUpdateChatList,
  592. conversationId: conversationId.current,
  593. isResponding,
  594. setIsResponding,
  595. handleSend,
  596. suggestedQuestions,
  597. handleRestart,
  598. handleStop,
  599. handleAnnotationEdited,
  600. handleAnnotationAdded,
  601. handleAnnotationRemoved,
  602. }
  603. }