|
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, |
|
} |
|
} |
|
|