mirror of
https://git.mirrors.martin98.com/https://github.com/langgenius/dify.git
synced 2025-05-07 02:39:00 +08:00
415 lines
14 KiB
TypeScript
415 lines
14 KiB
TypeScript
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 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 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)
|
|
|
|
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)
|
|
|
|
if (suggestedQuestionsAbortControllerRef.current)
|
|
suggestedQuestionsAbortControllerRef.current.abort()
|
|
}, [handleResponding, stopChat])
|
|
|
|
const handleRestart = useCallback(() => {
|
|
conversationId.current = ''
|
|
taskIdRef.current = ''
|
|
handleStop()
|
|
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,
|
|
])
|
|
|
|
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,
|
|
}
|
|
}
|