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 21KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654
  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. onGetConvesationMessages?: (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 connversationId = 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. connversationId.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. onGetConvesationMessages,
  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: connversationId.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. connversationId.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(connversationId.current)
  293. if (connversationId.current && !hasStopResponded.current && onGetConvesationMessages) {
  294. const { data }: any = await onGetConvesationMessages(
  295. connversationId.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: connversationId.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. const { data }: any = await onGetSuggestedQuestions(
  341. responseItem.id,
  342. newAbortController => suggestedQuestionsAbortControllerRef.current = newAbortController,
  343. )
  344. setSuggestQuestions(data)
  345. }
  346. },
  347. onFile(file) {
  348. const lastThought = responseItem.agent_thoughts?.[responseItem.agent_thoughts?.length - 1]
  349. if (lastThought)
  350. responseItem.agent_thoughts![responseItem.agent_thoughts!.length - 1].message_files = [...(lastThought as any).message_files, file]
  351. updateCurrentQA({
  352. responseItem,
  353. questionId,
  354. placeholderAnswerId,
  355. questionItem,
  356. })
  357. },
  358. onThought(thought) {
  359. isAgentMode = true
  360. const response = responseItem as any
  361. if (thought.message_id && !hasSetResponseId)
  362. response.id = thought.message_id
  363. if (response.agent_thoughts.length === 0) {
  364. response.agent_thoughts.push(thought)
  365. }
  366. else {
  367. const lastThought = response.agent_thoughts[response.agent_thoughts.length - 1]
  368. // thought changed but still the same thought, so update.
  369. if (lastThought.id === thought.id) {
  370. thought.thought = lastThought.thought
  371. thought.message_files = lastThought.message_files
  372. responseItem.agent_thoughts![response.agent_thoughts.length - 1] = thought
  373. }
  374. else {
  375. responseItem.agent_thoughts!.push(thought)
  376. }
  377. }
  378. updateCurrentQA({
  379. responseItem,
  380. questionId,
  381. placeholderAnswerId,
  382. questionItem,
  383. })
  384. },
  385. onMessageEnd: (messageEnd) => {
  386. if (messageEnd.metadata?.annotation_reply) {
  387. responseItem.id = messageEnd.id
  388. responseItem.annotation = ({
  389. id: messageEnd.metadata.annotation_reply.id,
  390. authorName: messageEnd.metadata.annotation_reply.account.name,
  391. })
  392. const baseState = chatListRef.current.filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId)
  393. const newListWithAnswer = produce(
  394. baseState,
  395. (draft) => {
  396. if (!draft.find(item => item.id === questionId))
  397. draft.push({ ...questionItem })
  398. draft.push({
  399. ...responseItem,
  400. })
  401. })
  402. handleUpdateChatList(newListWithAnswer)
  403. return
  404. }
  405. responseItem.citation = messageEnd.metadata?.retriever_resources || []
  406. const newListWithAnswer = produce(
  407. chatListRef.current.filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId),
  408. (draft) => {
  409. if (!draft.find(item => item.id === questionId))
  410. draft.push({ ...questionItem })
  411. draft.push({ ...responseItem })
  412. })
  413. handleUpdateChatList(newListWithAnswer)
  414. },
  415. onMessageReplace: (messageReplace) => {
  416. responseItem.content = messageReplace.answer
  417. },
  418. onError() {
  419. handleResponding(false)
  420. const newChatList = produce(chatListRef.current, (draft) => {
  421. draft.splice(draft.findIndex(item => item.id === placeholderAnswerId), 1)
  422. })
  423. handleUpdateChatList(newChatList)
  424. },
  425. onWorkflowStarted: ({ workflow_run_id, task_id }) => {
  426. taskIdRef.current = task_id
  427. responseItem.workflow_run_id = workflow_run_id
  428. responseItem.workflowProcess = {
  429. status: WorkflowRunningStatus.Running,
  430. tracing: [],
  431. }
  432. handleUpdateChatList(produce(chatListRef.current, (draft) => {
  433. const currentIndex = draft.findIndex(item => item.id === responseItem.id)
  434. draft[currentIndex] = {
  435. ...draft[currentIndex],
  436. ...responseItem,
  437. }
  438. }))
  439. },
  440. onWorkflowFinished: ({ data }) => {
  441. responseItem.workflowProcess!.status = data.status as WorkflowRunningStatus
  442. handleUpdateChatList(produce(chatListRef.current, (draft) => {
  443. const currentIndex = draft.findIndex(item => item.id === responseItem.id)
  444. draft[currentIndex] = {
  445. ...draft[currentIndex],
  446. ...responseItem,
  447. }
  448. }))
  449. },
  450. onIterationStart: ({ data }) => {
  451. responseItem.workflowProcess!.tracing!.push({
  452. ...data,
  453. status: WorkflowRunningStatus.Running,
  454. } as any)
  455. handleUpdateChatList(produce(chatListRef.current, (draft) => {
  456. const currentIndex = draft.findIndex(item => item.id === responseItem.id)
  457. draft[currentIndex] = {
  458. ...draft[currentIndex],
  459. ...responseItem,
  460. }
  461. }))
  462. isInIteration = true
  463. },
  464. onIterationFinish: ({ data }) => {
  465. const tracing = responseItem.workflowProcess!.tracing!
  466. tracing[tracing.length - 1] = {
  467. ...tracing[tracing.length - 1],
  468. ...data,
  469. status: WorkflowRunningStatus.Succeeded,
  470. } as any
  471. handleUpdateChatList(produce(chatListRef.current, (draft) => {
  472. const currentIndex = draft.findIndex(item => item.id === responseItem.id)
  473. draft[currentIndex] = {
  474. ...draft[currentIndex],
  475. ...responseItem,
  476. }
  477. }))
  478. isInIteration = false
  479. },
  480. onNodeStarted: ({ data }) => {
  481. if (isInIteration)
  482. return
  483. responseItem.workflowProcess!.tracing!.push({
  484. ...data,
  485. status: WorkflowRunningStatus.Running,
  486. } as any)
  487. handleUpdateChatList(produce(chatListRef.current, (draft) => {
  488. const currentIndex = draft.findIndex(item => item.id === responseItem.id)
  489. draft[currentIndex] = {
  490. ...draft[currentIndex],
  491. ...responseItem,
  492. }
  493. }))
  494. },
  495. onNodeFinished: ({ data }) => {
  496. if (isInIteration)
  497. return
  498. const currentIndex = responseItem.workflowProcess!.tracing!.findIndex(item => item.node_id === data.node_id)
  499. responseItem.workflowProcess!.tracing[currentIndex] = data as any
  500. handleUpdateChatList(produce(chatListRef.current, (draft) => {
  501. const currentIndex = draft.findIndex(item => item.id === responseItem.id)
  502. draft[currentIndex] = {
  503. ...draft[currentIndex],
  504. ...responseItem,
  505. }
  506. }))
  507. },
  508. onTTSChunk: (messageId: string, audio: string) => {
  509. if (!audio || audio === '')
  510. return
  511. player.playAudioWithAudio(audio, true)
  512. AudioPlayerManager.getInstance().resetMsgId(messageId)
  513. },
  514. onTTSEnd: (messageId: string, audio: string) => {
  515. player.playAudioWithAudio(audio, false)
  516. },
  517. })
  518. return true
  519. }, [
  520. checkPromptVariables,
  521. config?.suggested_questions_after_answer,
  522. updateCurrentQA,
  523. t,
  524. notify,
  525. promptVariablesConfig,
  526. handleUpdateChatList,
  527. handleResponding,
  528. formatTime,
  529. ])
  530. const handleAnnotationEdited = useCallback((query: string, answer: string, index: number) => {
  531. handleUpdateChatList(chatListRef.current.map((item, i) => {
  532. if (i === index - 1) {
  533. return {
  534. ...item,
  535. content: query,
  536. }
  537. }
  538. if (i === index) {
  539. return {
  540. ...item,
  541. content: answer,
  542. annotation: {
  543. ...item.annotation,
  544. logAnnotation: undefined,
  545. } as any,
  546. }
  547. }
  548. return item
  549. }))
  550. }, [handleUpdateChatList])
  551. const handleAnnotationAdded = useCallback((annotationId: string, authorName: string, query: string, answer: string, index: number) => {
  552. handleUpdateChatList(chatListRef.current.map((item, i) => {
  553. if (i === index - 1) {
  554. return {
  555. ...item,
  556. content: query,
  557. }
  558. }
  559. if (i === index) {
  560. const answerItem = {
  561. ...item,
  562. content: item.content,
  563. annotation: {
  564. id: annotationId,
  565. authorName,
  566. logAnnotation: {
  567. content: answer,
  568. account: {
  569. id: '',
  570. name: authorName,
  571. email: '',
  572. },
  573. },
  574. } as Annotation,
  575. }
  576. return answerItem
  577. }
  578. return item
  579. }))
  580. }, [handleUpdateChatList])
  581. const handleAnnotationRemoved = useCallback((index: number) => {
  582. handleUpdateChatList(chatListRef.current.map((item, i) => {
  583. if (i === index) {
  584. return {
  585. ...item,
  586. content: item.content,
  587. annotation: {
  588. ...(item.annotation || {}),
  589. id: '',
  590. } as Annotation,
  591. }
  592. }
  593. return item
  594. }))
  595. }, [handleUpdateChatList])
  596. return {
  597. chatList,
  598. setChatList,
  599. conversationId: connversationId.current,
  600. isResponding,
  601. setIsResponding,
  602. handleSend,
  603. suggestedQuestions,
  604. handleRestart,
  605. handleStop,
  606. handleAnnotationEdited,
  607. handleAnnotationAdded,
  608. handleAnnotationRemoved,
  609. }
  610. }