hooks.ts 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659
  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. let isInIteration = false
  222. handleResponding(true)
  223. hasStopResponded.current = false
  224. const bodyParams = {
  225. response_mode: 'streaming',
  226. conversation_id: conversationId.current,
  227. ...data,
  228. }
  229. if (bodyParams?.files?.length) {
  230. bodyParams.files = bodyParams.files.map((item: VisionFile) => {
  231. if (item.transfer_method === TransferMethod.local_file) {
  232. return {
  233. ...item,
  234. url: '',
  235. }
  236. }
  237. return item
  238. })
  239. }
  240. let isAgentMode = false
  241. let hasSetResponseId = false
  242. let ttsUrl = ''
  243. let ttsIsPublic = false
  244. if (params.token) {
  245. ttsUrl = '/text-to-audio'
  246. ttsIsPublic = true
  247. }
  248. else if (params.appId) {
  249. if (pathname.search('explore/installed') > -1)
  250. ttsUrl = `/installed-apps/${params.appId}/text-to-audio`
  251. else
  252. ttsUrl = `/apps/${params.appId}/text-to-audio`
  253. }
  254. const player = AudioPlayerManager.getInstance().getAudioPlayer(ttsUrl, ttsIsPublic, uuidV4(), 'none', 'none', (_: any): any => {})
  255. ssePost(
  256. url,
  257. {
  258. body: bodyParams,
  259. },
  260. {
  261. isPublicAPI,
  262. onData: (message: string, isFirstMessage: boolean, { conversationId: newConversationId, messageId, taskId }: any) => {
  263. if (!isAgentMode) {
  264. responseItem.content = responseItem.content + message
  265. }
  266. else {
  267. const lastThought = responseItem.agent_thoughts?.[responseItem.agent_thoughts?.length - 1]
  268. if (lastThought)
  269. lastThought.thought = lastThought.thought + message // need immer setAutoFreeze
  270. }
  271. if (messageId && !hasSetResponseId) {
  272. responseItem.id = messageId
  273. hasSetResponseId = true
  274. }
  275. if (isFirstMessage && newConversationId)
  276. conversationId.current = newConversationId
  277. taskIdRef.current = taskId
  278. if (messageId)
  279. responseItem.id = messageId
  280. updateCurrentQA({
  281. responseItem,
  282. questionId,
  283. placeholderAnswerId,
  284. questionItem,
  285. })
  286. },
  287. async onCompleted(hasError?: boolean) {
  288. handleResponding(false)
  289. if (hasError)
  290. return
  291. if (onConversationComplete)
  292. onConversationComplete(conversationId.current)
  293. if (conversationId.current && !hasStopResponded.current && onGetConversationMessages) {
  294. const { data }: any = await onGetConversationMessages(
  295. conversationId.current,
  296. newAbortController => conversationMessagesAbortControllerRef.current = newAbortController,
  297. )
  298. const newResponseItem = data.find((item: any) => item.id === responseItem.id)
  299. if (!newResponseItem)
  300. return
  301. const newChatList = produce(chatListRef.current, (draft) => {
  302. const index = draft.findIndex(item => item.id === responseItem.id)
  303. if (index !== -1) {
  304. const requestion = draft[index - 1]
  305. draft[index - 1] = {
  306. ...requestion,
  307. }
  308. draft[index] = {
  309. ...draft[index],
  310. content: newResponseItem.answer,
  311. log: [
  312. ...newResponseItem.message,
  313. ...(newResponseItem.message[newResponseItem.message.length - 1].role !== 'assistant'
  314. ? [
  315. {
  316. role: 'assistant',
  317. text: newResponseItem.answer,
  318. files: newResponseItem.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [],
  319. },
  320. ]
  321. : []),
  322. ],
  323. more: {
  324. time: formatTime(newResponseItem.created_at, 'hh:mm A'),
  325. tokens: newResponseItem.answer_tokens + newResponseItem.message_tokens,
  326. latency: newResponseItem.provider_response_latency.toFixed(2),
  327. },
  328. // for agent log
  329. conversationId: conversationId.current,
  330. input: {
  331. inputs: newResponseItem.inputs,
  332. query: newResponseItem.query,
  333. },
  334. }
  335. }
  336. })
  337. handleUpdateChatList(newChatList)
  338. }
  339. if (config?.suggested_questions_after_answer?.enabled && !hasStopResponded.current && onGetSuggestedQuestions) {
  340. try {
  341. const { data }: any = await onGetSuggestedQuestions(
  342. responseItem.id,
  343. newAbortController => suggestedQuestionsAbortControllerRef.current = newAbortController,
  344. )
  345. setSuggestQuestions(data)
  346. }
  347. catch (e) {
  348. setSuggestQuestions([])
  349. }
  350. }
  351. },
  352. onFile(file) {
  353. const lastThought = responseItem.agent_thoughts?.[responseItem.agent_thoughts?.length - 1]
  354. if (lastThought)
  355. responseItem.agent_thoughts![responseItem.agent_thoughts!.length - 1].message_files = [...(lastThought as any).message_files, file]
  356. updateCurrentQA({
  357. responseItem,
  358. questionId,
  359. placeholderAnswerId,
  360. questionItem,
  361. })
  362. },
  363. onThought(thought) {
  364. isAgentMode = true
  365. const response = responseItem as any
  366. if (thought.message_id && !hasSetResponseId)
  367. response.id = thought.message_id
  368. if (response.agent_thoughts.length === 0) {
  369. response.agent_thoughts.push(thought)
  370. }
  371. else {
  372. const lastThought = response.agent_thoughts[response.agent_thoughts.length - 1]
  373. // thought changed but still the same thought, so update.
  374. if (lastThought.id === thought.id) {
  375. thought.thought = lastThought.thought
  376. thought.message_files = lastThought.message_files
  377. responseItem.agent_thoughts![response.agent_thoughts.length - 1] = thought
  378. }
  379. else {
  380. responseItem.agent_thoughts!.push(thought)
  381. }
  382. }
  383. updateCurrentQA({
  384. responseItem,
  385. questionId,
  386. placeholderAnswerId,
  387. questionItem,
  388. })
  389. },
  390. onMessageEnd: (messageEnd) => {
  391. if (messageEnd.metadata?.annotation_reply) {
  392. responseItem.id = messageEnd.id
  393. responseItem.annotation = ({
  394. id: messageEnd.metadata.annotation_reply.id,
  395. authorName: messageEnd.metadata.annotation_reply.account.name,
  396. })
  397. const baseState = chatListRef.current.filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId)
  398. const newListWithAnswer = produce(
  399. baseState,
  400. (draft) => {
  401. if (!draft.find(item => item.id === questionId))
  402. draft.push({ ...questionItem })
  403. draft.push({
  404. ...responseItem,
  405. })
  406. })
  407. handleUpdateChatList(newListWithAnswer)
  408. return
  409. }
  410. responseItem.citation = messageEnd.metadata?.retriever_resources || []
  411. const newListWithAnswer = produce(
  412. chatListRef.current.filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId),
  413. (draft) => {
  414. if (!draft.find(item => item.id === questionId))
  415. draft.push({ ...questionItem })
  416. draft.push({ ...responseItem })
  417. })
  418. handleUpdateChatList(newListWithAnswer)
  419. },
  420. onMessageReplace: (messageReplace) => {
  421. responseItem.content = messageReplace.answer
  422. },
  423. onError() {
  424. handleResponding(false)
  425. const newChatList = produce(chatListRef.current, (draft) => {
  426. draft.splice(draft.findIndex(item => item.id === placeholderAnswerId), 1)
  427. })
  428. handleUpdateChatList(newChatList)
  429. },
  430. onWorkflowStarted: ({ workflow_run_id, task_id }) => {
  431. taskIdRef.current = task_id
  432. responseItem.workflow_run_id = workflow_run_id
  433. responseItem.workflowProcess = {
  434. status: WorkflowRunningStatus.Running,
  435. tracing: [],
  436. }
  437. handleUpdateChatList(produce(chatListRef.current, (draft) => {
  438. const currentIndex = draft.findIndex(item => item.id === responseItem.id)
  439. draft[currentIndex] = {
  440. ...draft[currentIndex],
  441. ...responseItem,
  442. }
  443. }))
  444. },
  445. onWorkflowFinished: ({ data }) => {
  446. responseItem.workflowProcess!.status = data.status as WorkflowRunningStatus
  447. handleUpdateChatList(produce(chatListRef.current, (draft) => {
  448. const currentIndex = draft.findIndex(item => item.id === responseItem.id)
  449. draft[currentIndex] = {
  450. ...draft[currentIndex],
  451. ...responseItem,
  452. }
  453. }))
  454. },
  455. onIterationStart: ({ data }) => {
  456. responseItem.workflowProcess!.tracing!.push({
  457. ...data,
  458. status: WorkflowRunningStatus.Running,
  459. } as any)
  460. handleUpdateChatList(produce(chatListRef.current, (draft) => {
  461. const currentIndex = draft.findIndex(item => item.id === responseItem.id)
  462. draft[currentIndex] = {
  463. ...draft[currentIndex],
  464. ...responseItem,
  465. }
  466. }))
  467. isInIteration = true
  468. },
  469. onIterationFinish: ({ data }) => {
  470. const tracing = responseItem.workflowProcess!.tracing!
  471. tracing[tracing.length - 1] = {
  472. ...tracing[tracing.length - 1],
  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. isInIteration = false
  484. },
  485. onNodeStarted: ({ data }) => {
  486. if (isInIteration)
  487. return
  488. responseItem.workflowProcess!.tracing!.push({
  489. ...data,
  490. status: WorkflowRunningStatus.Running,
  491. } as any)
  492. handleUpdateChatList(produce(chatListRef.current, (draft) => {
  493. const currentIndex = draft.findIndex(item => item.id === responseItem.id)
  494. draft[currentIndex] = {
  495. ...draft[currentIndex],
  496. ...responseItem,
  497. }
  498. }))
  499. },
  500. onNodeFinished: ({ data }) => {
  501. if (isInIteration)
  502. return
  503. const currentIndex = responseItem.workflowProcess!.tracing!.findIndex(item => item.node_id === data.node_id)
  504. responseItem.workflowProcess!.tracing[currentIndex] = data as any
  505. handleUpdateChatList(produce(chatListRef.current, (draft) => {
  506. const currentIndex = draft.findIndex(item => item.id === responseItem.id)
  507. draft[currentIndex] = {
  508. ...draft[currentIndex],
  509. ...responseItem,
  510. }
  511. }))
  512. },
  513. onTTSChunk: (messageId: string, audio: string) => {
  514. if (!audio || audio === '')
  515. return
  516. player.playAudioWithAudio(audio, true)
  517. AudioPlayerManager.getInstance().resetMsgId(messageId)
  518. },
  519. onTTSEnd: (messageId: string, audio: string) => {
  520. player.playAudioWithAudio(audio, false)
  521. },
  522. })
  523. return true
  524. }, [
  525. checkPromptVariables,
  526. config?.suggested_questions_after_answer,
  527. updateCurrentQA,
  528. t,
  529. notify,
  530. promptVariablesConfig,
  531. handleUpdateChatList,
  532. handleResponding,
  533. formatTime,
  534. ])
  535. const handleAnnotationEdited = useCallback((query: string, answer: string, index: number) => {
  536. handleUpdateChatList(chatListRef.current.map((item, i) => {
  537. if (i === index - 1) {
  538. return {
  539. ...item,
  540. content: query,
  541. }
  542. }
  543. if (i === index) {
  544. return {
  545. ...item,
  546. content: answer,
  547. annotation: {
  548. ...item.annotation,
  549. logAnnotation: undefined,
  550. } as any,
  551. }
  552. }
  553. return item
  554. }))
  555. }, [handleUpdateChatList])
  556. const handleAnnotationAdded = useCallback((annotationId: string, authorName: string, query: string, answer: string, index: number) => {
  557. handleUpdateChatList(chatListRef.current.map((item, i) => {
  558. if (i === index - 1) {
  559. return {
  560. ...item,
  561. content: query,
  562. }
  563. }
  564. if (i === index) {
  565. const answerItem = {
  566. ...item,
  567. content: item.content,
  568. annotation: {
  569. id: annotationId,
  570. authorName,
  571. logAnnotation: {
  572. content: answer,
  573. account: {
  574. id: '',
  575. name: authorName,
  576. email: '',
  577. },
  578. },
  579. } as Annotation,
  580. }
  581. return answerItem
  582. }
  583. return item
  584. }))
  585. }, [handleUpdateChatList])
  586. const handleAnnotationRemoved = useCallback((index: number) => {
  587. handleUpdateChatList(chatListRef.current.map((item, i) => {
  588. if (i === index) {
  589. return {
  590. ...item,
  591. content: item.content,
  592. annotation: {
  593. ...(item.annotation || {}),
  594. id: '',
  595. } as Annotation,
  596. }
  597. }
  598. return item
  599. }))
  600. }, [handleUpdateChatList])
  601. return {
  602. chatList,
  603. setChatList,
  604. conversationId: conversationId.current,
  605. isResponding,
  606. setIsResponding,
  607. handleSend,
  608. suggestedQuestions,
  609. handleRestart,
  610. handleStop,
  611. handleAnnotationEdited,
  612. handleAnnotationAdded,
  613. handleAnnotationRemoved,
  614. }
  615. }