| import { |
| useCallback, |
| useEffect, |
| useRef, |
| useState, |
| } from 'react' |
| import { useTranslation } from 'react-i18next' |
| import { produce, setAutoFreeze } from 'immer' |
| import { uniqBy } from 'lodash-es' |
| import { useWorkflowRun } from '../../hooks' |
| import { NodeRunningStatus, WorkflowRunningStatus } from '../../types' |
| import { useWorkflowStore } from '../../store' |
| import { DEFAULT_ITER_TIMES } from '../../constants' |
| import type { |
| ChatItem, |
| Inputs, |
| } from '@/app/components/base/chat/types' |
| import type { InputForm } from '@/app/components/base/chat/chat/type' |
| import { |
| getProcessedInputs, |
| processOpeningStatement, |
| } from '@/app/components/base/chat/chat/utils' |
| import { useToastContext } from '@/app/components/base/toast' |
| import { TransferMethod } from '@/types/app' |
| import { |
| getProcessedFiles, |
| getProcessedFilesFromResponse, |
| } from '@/app/components/base/file-uploader/utils' |
| import type { FileEntity } from '@/app/components/base/file-uploader/types' |
|
|
| type GetAbortController = (abortController: AbortController) => void |
| type SendCallback = { |
| onGetSuggestedQuestions?: (responseItemId: string, getAbortController: GetAbortController) => Promise<any> |
| } |
| export const useChat = ( |
| config: any, |
| formSettings?: { |
| inputs: Inputs |
| inputsForm: InputForm[] |
| }, |
| prevChatList?: ChatItem[], |
| stopChat?: (taskId: string) => void, |
| ) => { |
| const { t } = useTranslation() |
| const { notify } = useToastContext() |
| const { handleRun } = useWorkflowRun() |
| const hasStopResponded = useRef(false) |
| const workflowStore = useWorkflowStore() |
| const conversationId = useRef('') |
| const taskIdRef = useRef('') |
| const [chatList, setChatList] = useState<ChatItem[]>(prevChatList || []) |
| const chatListRef = useRef<ChatItem[]>(prevChatList || []) |
| const [isResponding, setIsResponding] = useState(false) |
| const isRespondingRef = useRef(false) |
| const [suggestedQuestions, setSuggestQuestions] = useState<string[]>([]) |
| const suggestedQuestionsAbortControllerRef = useRef<AbortController | null>(null) |
|
|
| const { |
| setIterTimes, |
| } = workflowStore.getState() |
| useEffect(() => { |
| setAutoFreeze(false) |
| return () => { |
| setAutoFreeze(true) |
| } |
| }, []) |
|
|
| const handleUpdateChatList = useCallback((newChatList: ChatItem[]) => { |
| setChatList(newChatList) |
| chatListRef.current = newChatList |
| }, []) |
|
|
| const handleResponding = useCallback((isResponding: boolean) => { |
| setIsResponding(isResponding) |
| isRespondingRef.current = isResponding |
| }, []) |
|
|
| const getIntroduction = useCallback((str: string) => { |
| return processOpeningStatement(str, formSettings?.inputs || {}, formSettings?.inputsForm || []) |
| }, [formSettings?.inputs, formSettings?.inputsForm]) |
| useEffect(() => { |
| if (config?.opening_statement) { |
| handleUpdateChatList(produce(chatListRef.current, (draft) => { |
| const index = draft.findIndex(item => item.isOpeningStatement) |
|
|
| if (index > -1) { |
| draft[index] = { |
| ...draft[index], |
| content: getIntroduction(config.opening_statement), |
| suggestedQuestions: config.suggested_questions, |
| } |
| } |
| else { |
| draft.unshift({ |
| id: `${Date.now()}`, |
| content: getIntroduction(config.opening_statement), |
| isAnswer: true, |
| isOpeningStatement: true, |
| suggestedQuestions: config.suggested_questions, |
| }) |
| } |
| })) |
| } |
| }, [config?.opening_statement, getIntroduction, config?.suggested_questions, handleUpdateChatList]) |
|
|
| const handleStop = useCallback(() => { |
| hasStopResponded.current = true |
| handleResponding(false) |
| if (stopChat && taskIdRef.current) |
| stopChat(taskIdRef.current) |
| setIterTimes(DEFAULT_ITER_TIMES) |
| if (suggestedQuestionsAbortControllerRef.current) |
| suggestedQuestionsAbortControllerRef.current.abort() |
| }, [handleResponding, setIterTimes, stopChat]) |
|
|
| const handleRestart = useCallback(() => { |
| conversationId.current = '' |
| taskIdRef.current = '' |
| handleStop() |
| setIterTimes(DEFAULT_ITER_TIMES) |
| const newChatList = config?.opening_statement |
| ? [{ |
| id: `${Date.now()}`, |
| content: config.opening_statement, |
| isAnswer: true, |
| isOpeningStatement: true, |
| suggestedQuestions: config.suggested_questions, |
| }] |
| : [] |
| handleUpdateChatList(newChatList) |
| setSuggestQuestions([]) |
| }, [ |
| config, |
| handleStop, |
| handleUpdateChatList, |
| setIterTimes, |
| ]) |
|
|
| const updateCurrentQA = useCallback(({ |
| responseItem, |
| questionId, |
| placeholderAnswerId, |
| questionItem, |
| }: { |
| responseItem: ChatItem |
| questionId: string |
| placeholderAnswerId: string |
| questionItem: ChatItem |
| }) => { |
| const newListWithAnswer = produce( |
| chatListRef.current.filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId), |
| (draft) => { |
| if (!draft.find(item => item.id === questionId)) |
| draft.push({ ...questionItem }) |
|
|
| draft.push({ ...responseItem }) |
| }) |
| handleUpdateChatList(newListWithAnswer) |
| }, [handleUpdateChatList]) |
|
|
| const handleSend = useCallback(( |
| params: { |
| query: string |
| files?: FileEntity[] |
| [key: string]: any |
| }, |
| { |
| onGetSuggestedQuestions, |
| }: SendCallback, |
| ) => { |
| if (isRespondingRef.current) { |
| notify({ type: 'info', message: t('appDebug.errorMessage.waitForResponse') }) |
| return false |
| } |
|
|
| const questionId = `question-${Date.now()}` |
| const questionItem = { |
| id: questionId, |
| content: params.query, |
| isAnswer: false, |
| message_files: params.files, |
| } |
|
|
| const placeholderAnswerId = `answer-placeholder-${Date.now()}` |
| const placeholderAnswerItem = { |
| id: placeholderAnswerId, |
| content: '', |
| isAnswer: true, |
| } |
|
|
| const newList = [...chatListRef.current, questionItem, placeholderAnswerItem] |
| handleUpdateChatList(newList) |
|
|
| |
| const responseItem: ChatItem = { |
| id: placeholderAnswerId, |
| content: '', |
| agent_thoughts: [], |
| message_files: [], |
| isAnswer: true, |
| } |
|
|
| handleResponding(true) |
|
|
| const { files, inputs, ...restParams } = params |
| const bodyParams = { |
| files: getProcessedFiles(files || []), |
| inputs: getProcessedInputs(inputs || {}, formSettings?.inputsForm || []), |
| ...restParams, |
| } |
| if (bodyParams?.files?.length) { |
| bodyParams.files = bodyParams.files.map((item) => { |
| if (item.transfer_method === TransferMethod.local_file) { |
| return { |
| ...item, |
| url: '', |
| } |
| } |
| return item |
| }) |
| } |
|
|
| let hasSetResponseId = false |
|
|
| handleRun( |
| bodyParams, |
| { |
| onData: (message: string, isFirstMessage: boolean, { conversationId: newConversationId, messageId, taskId }: any) => { |
| responseItem.content = responseItem.content + message |
|
|
| if (messageId && !hasSetResponseId) { |
| responseItem.id = messageId |
| hasSetResponseId = true |
| } |
|
|
| if (isFirstMessage && newConversationId) |
| conversationId.current = newConversationId |
|
|
| taskIdRef.current = taskId |
| if (messageId) |
| responseItem.id = messageId |
|
|
| updateCurrentQA({ |
| responseItem, |
| questionId, |
| placeholderAnswerId, |
| questionItem, |
| }) |
| }, |
| async onCompleted(hasError?: boolean, errorMessage?: string) { |
| handleResponding(false) |
|
|
| if (hasError) { |
| if (errorMessage) { |
| responseItem.content = errorMessage |
| responseItem.isError = true |
| const newListWithAnswer = produce( |
| chatListRef.current.filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId), |
| (draft) => { |
| if (!draft.find(item => item.id === questionId)) |
| draft.push({ ...questionItem }) |
|
|
| draft.push({ ...responseItem }) |
| }) |
| handleUpdateChatList(newListWithAnswer) |
| } |
| return |
| } |
|
|
| if (config?.suggested_questions_after_answer?.enabled && !hasStopResponded.current && onGetSuggestedQuestions) { |
| try { |
| const { data }: any = await onGetSuggestedQuestions( |
| responseItem.id, |
| newAbortController => suggestedQuestionsAbortControllerRef.current = newAbortController, |
| ) |
| setSuggestQuestions(data) |
| } |
| catch (error) { |
| setSuggestQuestions([]) |
| } |
| } |
| }, |
| onMessageEnd: (messageEnd) => { |
| responseItem.citation = messageEnd.metadata?.retriever_resources || [] |
| const processedFilesFromResponse = getProcessedFilesFromResponse(messageEnd.files || []) |
| responseItem.allFiles = uniqBy([...(responseItem.allFiles || []), ...(processedFilesFromResponse || [])], 'id') |
|
|
| const newListWithAnswer = produce( |
| chatListRef.current.filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId), |
| (draft) => { |
| if (!draft.find(item => item.id === questionId)) |
| draft.push({ ...questionItem }) |
|
|
| draft.push({ ...responseItem }) |
| }) |
| handleUpdateChatList(newListWithAnswer) |
| }, |
| onMessageReplace: (messageReplace) => { |
| responseItem.content = messageReplace.answer |
| }, |
| onError() { |
| handleResponding(false) |
| }, |
| onWorkflowStarted: ({ workflow_run_id, task_id }) => { |
| taskIdRef.current = task_id |
| responseItem.workflow_run_id = workflow_run_id |
| responseItem.workflowProcess = { |
| status: WorkflowRunningStatus.Running, |
| tracing: [], |
| } |
| handleUpdateChatList(produce(chatListRef.current, (draft) => { |
| const currentIndex = draft.findIndex(item => item.id === responseItem.id) |
| draft[currentIndex] = { |
| ...draft[currentIndex], |
| ...responseItem, |
| } |
| })) |
| }, |
| onWorkflowFinished: ({ data }) => { |
| responseItem.workflowProcess!.status = data.status as WorkflowRunningStatus |
| handleUpdateChatList(produce(chatListRef.current, (draft) => { |
| const currentIndex = draft.findIndex(item => item.id === responseItem.id) |
| draft[currentIndex] = { |
| ...draft[currentIndex], |
| ...responseItem, |
| } |
| })) |
| }, |
| onIterationStart: ({ data }) => { |
| responseItem.workflowProcess!.tracing!.push({ |
| ...data, |
| status: NodeRunningStatus.Running, |
| details: [], |
| } as any) |
| handleUpdateChatList(produce(chatListRef.current, (draft) => { |
| const currentIndex = draft.findIndex(item => item.id === responseItem.id) |
| draft[currentIndex] = { |
| ...draft[currentIndex], |
| ...responseItem, |
| } |
| })) |
| }, |
| onIterationNext: ({ data }) => { |
| const tracing = responseItem.workflowProcess!.tracing! |
| const iterations = tracing.find(item => item.node_id === data.node_id |
| && (item.execution_metadata?.parallel_id === data.execution_metadata?.parallel_id || item.parallel_id === data.execution_metadata?.parallel_id))! |
| iterations.details!.push([]) |
|
|
| handleUpdateChatList(produce(chatListRef.current, (draft) => { |
| const currentIndex = draft.length - 1 |
| draft[currentIndex] = responseItem |
| })) |
| }, |
| onIterationFinish: ({ data }) => { |
| const tracing = responseItem.workflowProcess!.tracing! |
| const iterationsIndex = tracing.findIndex(item => item.node_id === data.node_id |
| && (item.execution_metadata?.parallel_id === data.execution_metadata?.parallel_id || item.parallel_id === data.execution_metadata?.parallel_id))! |
| tracing[iterationsIndex] = { |
| ...tracing[iterationsIndex], |
| ...data, |
| status: NodeRunningStatus.Succeeded, |
| } as any |
| handleUpdateChatList(produce(chatListRef.current, (draft) => { |
| const currentIndex = draft.length - 1 |
| draft[currentIndex] = responseItem |
| })) |
| }, |
| onNodeStarted: ({ data }) => { |
| if (data.iteration_id) |
| return |
|
|
| responseItem.workflowProcess!.tracing!.push({ |
| ...data, |
| status: NodeRunningStatus.Running, |
| } as any) |
| handleUpdateChatList(produce(chatListRef.current, (draft) => { |
| const currentIndex = draft.findIndex(item => item.id === responseItem.id) |
| draft[currentIndex] = { |
| ...draft[currentIndex], |
| ...responseItem, |
| } |
| })) |
| }, |
| onNodeFinished: ({ data }) => { |
| if (data.iteration_id) |
| return |
|
|
| const currentIndex = responseItem.workflowProcess!.tracing!.findIndex((item) => { |
| if (!item.execution_metadata?.parallel_id) |
| return item.node_id === data.node_id |
| return item.node_id === data.node_id && (item.execution_metadata?.parallel_id === data.execution_metadata?.parallel_id || item.parallel_id === data.execution_metadata?.parallel_id) |
| }) |
| responseItem.workflowProcess!.tracing[currentIndex] = { |
| ...(responseItem.workflowProcess!.tracing[currentIndex]?.extras |
| ? { extras: responseItem.workflowProcess!.tracing[currentIndex].extras } |
| : {}), |
| ...data, |
| } as any |
| handleUpdateChatList(produce(chatListRef.current, (draft) => { |
| const currentIndex = draft.findIndex(item => item.id === responseItem.id) |
| draft[currentIndex] = { |
| ...draft[currentIndex], |
| ...responseItem, |
| } |
| })) |
| }, |
| }, |
| ) |
| }, [handleRun, handleResponding, handleUpdateChatList, notify, t, updateCurrentQA, config.suggested_questions_after_answer?.enabled, formSettings]) |
|
|
| return { |
| conversationId: conversationId.current, |
| chatList, |
| chatListRef, |
| handleUpdateChatList, |
| handleSend, |
| handleStop, |
| handleRestart, |
| isResponding, |
| suggestedQuestions, |
| } |
| } |
|
|