hooks.ts 22 KB

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