You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

hooks.ts 23KB

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