diff --git a/web/app/components/workflow/header/run-and-history.tsx b/web/app/components/workflow/header/run-and-history.tsx index 6a480cd3d0..4d99083f4c 100644 --- a/web/app/components/workflow/header/run-and-history.tsx +++ b/web/app/components/workflow/header/run-and-history.tsx @@ -56,7 +56,6 @@ const PreviewMode = memo(() => { const { t } = useTranslation() const { handleRunInit } = useWorkflow() const runningStatus = useStore(s => s.runningStatus) - const isRunning = runningStatus === WorkflowRunningStatus.Running const handleClick = () => { handleRunInit() @@ -67,12 +66,12 @@ const PreviewMode = memo(() => { className={` flex items-center px-1.5 h-7 rounded-md text-[13px] font-medium text-primary-600 hover:bg-primary-50 cursor-pointer - ${isRunning && 'bg-primary-50 opacity-50 !cursor-not-allowed'} + ${runningStatus && 'bg-primary-50 opacity-50 !cursor-not-allowed'} `} - onClick={() => !isRunning && handleClick()} + onClick={() => !runningStatus && handleClick()} > { - isRunning + runningStatus ? ( <> {t('workflow.common.inPreview')} diff --git a/web/app/components/workflow/hooks.ts b/web/app/components/workflow/hooks.ts index 5e2021c6b0..e9ecf65fb2 100644 --- a/web/app/components/workflow/hooks.ts +++ b/web/app/components/workflow/hooks.ts @@ -36,6 +36,7 @@ import { syncWorkflowDraft } from '@/service/workflow' import { useFeaturesStore } from '@/app/components/base/features/hooks' import { useStore as useAppStore } from '@/app/components/app/store' import { ssePost } from '@/service/base' +import type { IOtherOptions } from '@/service/base' export const useIsChatMode = () => { const appDetail = useAppStore(s => s.appDetail) @@ -671,7 +672,7 @@ export const useWorkflow = () => { export const useWorkflowRun = () => { const store = useStoreApi() - return (params: any) => { + const run = useCallback((params: any, callback?: IOtherOptions) => { const { getNodes, setNodes, @@ -721,7 +722,10 @@ export const useWorkflowRun = () => { }) setNodes(newNodes) }, + ...callback, }, ) - } + }, [store]) + + return run } diff --git a/web/app/components/workflow/panel/debug-and-preview/chat-wrapper.tsx b/web/app/components/workflow/panel/debug-and-preview/chat-wrapper.tsx index 4aca3ed2b9..c7462e1094 100644 --- a/web/app/components/workflow/panel/debug-and-preview/chat-wrapper.tsx +++ b/web/app/components/workflow/panel/debug-and-preview/chat-wrapper.tsx @@ -1,23 +1,41 @@ +import { + memo, + useCallback, +} from 'react' +import { useStore } from '../../store' import UserInput from './user-input' +import { useChat } from './hooks' import Chat from '@/app/components/base/chat/chat' -import { useChat } from '@/app/components/base/chat/chat/hooks' +import type { OnSend } from '@/app/components/base/chat/types' const ChatWrapper = () => { const { + conversationId, + chatList, handleStop, isResponding, suggestedQuestions, + handleSend, } = useChat() + const doSend = useCallback((query, files) => { + handleSend({ + query, + files, + inputs: useStore.getState().inputs, + conversationId, + }) + }, [conversationId, handleSend]) + return ( {}} + chatContainerInnerClassName='pt-6' + chatFooterClassName='px-4' + chatFooterInnerClassName='pb-4' + onSend={doSend} onStopResponding={handleStop} chatNode={} allToolIcons={{}} @@ -26,4 +44,4 @@ const ChatWrapper = () => { ) } -export default ChatWrapper +export default memo(ChatWrapper) diff --git a/web/app/components/workflow/panel/debug-and-preview/hooks.ts b/web/app/components/workflow/panel/debug-and-preview/hooks.ts new file mode 100644 index 0000000000..480a91f164 --- /dev/null +++ b/web/app/components/workflow/panel/debug-and-preview/hooks.ts @@ -0,0 +1,192 @@ +import { + useCallback, + useEffect, + useRef, + useState, +} from 'react' +import { useTranslation } from 'react-i18next' +import { produce, setAutoFreeze } from 'immer' +import { useWorkflowRun } from '../../hooks' +import type { ChatItem } from '@/app/components/base/chat/types' +import { useToastContext } from '@/app/components/base/toast' +import { TransferMethod } from '@/types/app' +import type { VisionFile } from '@/types/app' + +export const useChat = ( + prevChatList?: ChatItem[], +) => { + const { t } = useTranslation() + const { notify } = useToastContext() + const run = useWorkflowRun() + const hasStopResponded = useRef(false) + const connversationId = useRef('') + const taskIdRef = useRef('') + const [chatList, setChatList] = useState(prevChatList || []) + const chatListRef = useRef(prevChatList || []) + const [isResponding, setIsResponding] = useState(false) + const isRespondingRef = useRef(false) + const [suggestedQuestions, setSuggestQuestions] = useState([]) + const stopAbortControllerRef = useRef(null) + const suggestedQuestionsAbortControllerRef = useRef(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 handleStop = useCallback(() => { + hasStopResponded.current = true + handleResponding(false) + }, [handleResponding]) + + 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: any) => { + 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: `${Date.now()}`, + content: '', + agent_thoughts: [], + message_files: [], + isAnswer: true, + } + + handleResponding(true) + + const bodyParams = { + conversation_id: connversationId.current, + ...params, + } + if (bodyParams?.files?.length) { + bodyParams.files = bodyParams.files.map((item: VisionFile) => { + if (item.transfer_method === TransferMethod.local_file) { + return { + ...item, + url: '', + } + } + return item + }) + } + + let hasSetResponseId = false + + run( + params, + { + 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) + connversationId.current = newConversationId + + taskIdRef.current = taskId + if (messageId) + responseItem.id = messageId + + updateCurrentQA({ + responseItem, + questionId, + placeholderAnswerId, + questionItem, + }) + }, + async onCompleted(hasError?: boolean) { + handleResponding(false) + }, + onMessageEnd: (messageEnd) => { + responseItem.citation = messageEnd.metadata?.retriever_resources || [] + + 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) + const newChatList = produce(chatListRef.current, (draft) => { + draft.splice(draft.findIndex(item => item.id === placeholderAnswerId), 1) + }) + handleUpdateChatList(newChatList) + }, + }, + ) + }, [run, handleResponding, handleUpdateChatList, notify, t, updateCurrentQA]) + + return { + conversationId: connversationId.current, + chatList, + handleSend, + handleStop, + isResponding, + suggestedQuestions, + } +} diff --git a/web/app/components/workflow/panel/debug-and-preview/user-input.tsx b/web/app/components/workflow/panel/debug-and-preview/user-input.tsx index 2fb37aa457..b2e43badc3 100644 --- a/web/app/components/workflow/panel/debug-and-preview/user-input.tsx +++ b/web/app/components/workflow/panel/debug-and-preview/user-input.tsx @@ -3,11 +3,27 @@ import { useState, } from 'react' import { useTranslation } from 'react-i18next' +import { useNodes } from 'reactflow' +import FormItem from '../../nodes/_base/components/before-run-form/form-item' +import { BlockEnum } from '../../types' +import { useStore } from '../../store' +import type { StartNodeType } from '../../nodes/start/types' import { ChevronDown } from '@/app/components/base/icons/src/vender/line/arrows' const UserInput = () => { const { t } = useTranslation() const [expanded, setExpanded] = useState(true) + const inputs = useStore(s => s.inputs) + const nodes = useNodes() + const startNode = nodes.find(node => node.data.type === BlockEnum.Start) + const variables = startNode?.data.variables || [] + + const handleValueChange = (variable: string, v: string) => { + useStore.getState().setInputs({ + ...inputs, + [variable]: v, + }) + } return (
{ { expanded && (
-
-
Service Name
- -
+ { + variables.map(variable => ( +
+ handleValueChange(variable.variable, v)} + /> +
+ )) + }
) } diff --git a/web/service/base.ts b/web/service/base.ts index b4703b3604..1ce66985f3 100644 --- a/web/service/base.ts +++ b/web/service/base.ts @@ -54,7 +54,7 @@ export type IOnNodeFinished = (nodeFinished: NodeFinishedResponse) => void export type IOnTextChunk = (textChunk: TextChunkResponse) => void export type IOnTextReplace = (textReplace: TextReplaceResponse) => void -type IOtherOptions = { +export type IOtherOptions = { isPublicAPI?: boolean bodyStringify?: boolean needAllResponseContent?: boolean