Spaces:
Paused
Paused
| 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) | |
| // answer | |
| 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, | |
| } | |
| } | |