mirror of
https://github.com/langgenius/dify.git
synced 2026-05-04 01:18:05 +08:00
chore(web): pre-align HITL frontend from build/feat/hitl
This commit is contained in:
@ -18,7 +18,7 @@ import {
|
||||
useStore,
|
||||
useWorkflowStore,
|
||||
} from '../../store'
|
||||
import { BlockEnum } from '../../types'
|
||||
import { BlockEnum, WorkflowRunningStatus } from '../../types'
|
||||
import ConversationVariableModal from './conversation-variable-modal'
|
||||
import Empty from './empty'
|
||||
import { useChat } from './hooks/use-chat'
|
||||
@ -84,7 +84,9 @@ const ChatWrapper = (
|
||||
suggestedQuestions,
|
||||
handleSend,
|
||||
handleRestart,
|
||||
setTargetMessageId,
|
||||
handleSwitchSibling,
|
||||
handleSubmitHumanInputForm,
|
||||
getHumanInputNodeData,
|
||||
} = useChat(
|
||||
config,
|
||||
{
|
||||
@ -121,6 +123,22 @@ const ChatWrapper = (
|
||||
doSend(editedQuestion ? editedQuestion.message : question.content, editedQuestion ? editedQuestion.files : question.message_files, true, isValidGeneratedAnswer(parentAnswer) ? parentAnswer : null)
|
||||
}, [chatList, doSend])
|
||||
|
||||
const doSwitchSibling = useCallback((siblingMessageId: string) => {
|
||||
handleSwitchSibling(siblingMessageId, {
|
||||
onGetSuggestedQuestions: (messageId, getAbortController) => fetchSuggestedQuestions(appDetail!.id, messageId, getAbortController),
|
||||
})
|
||||
}, [handleSwitchSibling, appDetail])
|
||||
|
||||
const doHumanInputFormSubmit = useCallback(async (formToken: string, formData: any) => {
|
||||
// Handle human input form submission
|
||||
await handleSubmitHumanInputForm(formToken, formData)
|
||||
}, [handleSubmitHumanInputForm])
|
||||
|
||||
const inputDisabled = useMemo(() => {
|
||||
const latestMessage = chatList[chatList.length - 1]
|
||||
return latestMessage?.isAnswer && (latestMessage.workflowProcess?.status === WorkflowRunningStatus.Paused)
|
||||
}, [chatList])
|
||||
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
eventEmitter?.useSubscription((v: any) => {
|
||||
if (v.type === EVENT_WORKFLOW_STOP)
|
||||
@ -168,6 +186,8 @@ const ChatWrapper = (
|
||||
inputsForm={(startVariables || []) as any}
|
||||
onRegenerate={doRegenerate}
|
||||
onStopResponding={handleStop}
|
||||
onHumanInputFormSubmit={doHumanInputFormSubmit}
|
||||
getHumanInputNodeData={getHumanInputNodeData}
|
||||
chatNode={(
|
||||
<>
|
||||
{showInputsFieldsPanel && <UserInput />}
|
||||
@ -182,7 +202,9 @@ const ChatWrapper = (
|
||||
suggestedQuestions={suggestedQuestions}
|
||||
showPromptLog
|
||||
chatAnswerContainerInner="!pr-2"
|
||||
switchSibling={setTargetMessageId}
|
||||
switchSibling={doSwitchSibling}
|
||||
inputDisabled={inputDisabled}
|
||||
hideAvatar
|
||||
/>
|
||||
{showConversationVariableModal && (
|
||||
<ConversationVariableModal
|
||||
|
||||
@ -18,6 +18,7 @@ export function useChatFlowControl({
|
||||
const setActiveTaskId = useStore(s => s.setActiveTaskId)
|
||||
const setHasStopResponded = useStore(s => s.setHasStopResponded)
|
||||
const setSuggestedQuestionsAbortController = useStore(s => s.setSuggestedQuestionsAbortController)
|
||||
const setWorkflowEventsAbortController = useStore(s => s.setWorkflowEventsAbortController)
|
||||
const invalidateRun = useStore(s => s.invalidateRun)
|
||||
const { handleNodeCancelRunningStatus } = useNodesInteractionsWithoutSync()
|
||||
const { handleEdgeCancelRunningStatus } = useEdgesInteractionsWithoutSync()
|
||||
@ -32,6 +33,7 @@ export function useChatFlowControl({
|
||||
const {
|
||||
activeTaskId,
|
||||
suggestedQuestionsAbortController,
|
||||
workflowEventsAbortController,
|
||||
workflowRunningData,
|
||||
setWorkflowRunningData,
|
||||
} = workflowStore.getState()
|
||||
@ -45,7 +47,10 @@ export function useChatFlowControl({
|
||||
setLoopTimes(DEFAULT_LOOP_TIMES)
|
||||
if (suggestedQuestionsAbortController)
|
||||
suggestedQuestionsAbortController.abort()
|
||||
if (workflowEventsAbortController)
|
||||
workflowEventsAbortController.abort()
|
||||
setSuggestedQuestionsAbortController(null)
|
||||
setWorkflowEventsAbortController(null)
|
||||
setActiveTaskId('')
|
||||
invalidateRun()
|
||||
if (isActiveRun && workflowRunningData) {
|
||||
@ -69,6 +74,7 @@ export function useChatFlowControl({
|
||||
workflowStore,
|
||||
setHasStopResponded,
|
||||
setSuggestedQuestionsAbortController,
|
||||
setWorkflowEventsAbortController,
|
||||
setActiveTaskId,
|
||||
invalidateRun,
|
||||
handleNodeCancelRunningStatus,
|
||||
|
||||
@ -0,0 +1,228 @@
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import { vi } from 'vitest'
|
||||
import { WorkflowRunningStatus } from '../../../types'
|
||||
import { useChatMessageSender } from './use-chat-message-sender'
|
||||
|
||||
const {
|
||||
mockSseGet,
|
||||
mockSubmitHumanInputForm,
|
||||
mockHandleRun,
|
||||
mockFetchInspectVars,
|
||||
mockInvalidAllLastRun,
|
||||
mockInvalidateSandboxFiles,
|
||||
} = vi.hoisted(() => ({
|
||||
mockSseGet: vi.fn(),
|
||||
mockSubmitHumanInputForm: vi.fn(),
|
||||
mockHandleRun: vi.fn(),
|
||||
mockFetchInspectVars: vi.fn(),
|
||||
mockInvalidAllLastRun: vi.fn(),
|
||||
mockInvalidateSandboxFiles: vi.fn(),
|
||||
}))
|
||||
|
||||
type ChatTreeNode = {
|
||||
id: string
|
||||
children?: ChatTreeNode[]
|
||||
workflowProcess?: {
|
||||
status: WorkflowRunningStatus
|
||||
tracing: unknown[]
|
||||
}
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
type ChatPreviewStoreState = {
|
||||
chatTree: ChatTreeNode[]
|
||||
updateChatTree: (updater: (current: ChatTreeNode[]) => ChatTreeNode[]) => void
|
||||
setConversationId: ReturnType<typeof vi.fn>
|
||||
setTargetMessageId: ReturnType<typeof vi.fn>
|
||||
setSuggestedQuestions: ReturnType<typeof vi.fn>
|
||||
setActiveTaskId: ReturnType<typeof vi.fn>
|
||||
setHasStopResponded: ReturnType<typeof vi.fn>
|
||||
setSuggestedQuestionsAbortController: ReturnType<typeof vi.fn>
|
||||
setWorkflowEventsAbortController: ReturnType<typeof vi.fn>
|
||||
startRun: ReturnType<typeof vi.fn>
|
||||
}
|
||||
|
||||
const mockStoreState: ChatPreviewStoreState = {
|
||||
chatTree: [],
|
||||
updateChatTree: () => {},
|
||||
setConversationId: vi.fn(),
|
||||
setTargetMessageId: vi.fn(),
|
||||
setSuggestedQuestions: vi.fn(),
|
||||
setActiveTaskId: vi.fn(),
|
||||
setHasStopResponded: vi.fn(),
|
||||
setSuggestedQuestionsAbortController: vi.fn(),
|
||||
setWorkflowEventsAbortController: vi.fn(),
|
||||
startRun: vi.fn(() => 1),
|
||||
}
|
||||
|
||||
const mockWorkflowStore = {
|
||||
getState: vi.fn(() => ({
|
||||
isResponding: false,
|
||||
activeRunId: 1,
|
||||
suggestedQuestionsAbortController: null,
|
||||
workflowEventsAbortController: null as AbortController | null,
|
||||
workflowRunningData: {
|
||||
result: {
|
||||
status: WorkflowRunningStatus.Running,
|
||||
},
|
||||
},
|
||||
hasStopResponded: false,
|
||||
})),
|
||||
}
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
useToastContext: () => ({
|
||||
notify: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/base', () => ({
|
||||
sseGet: (...args: unknown[]) => mockSseGet(...args),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/workflow', () => ({
|
||||
submitHumanInputForm: (...args: unknown[]) => mockSubmitHumanInputForm(...args),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-workflow', () => ({
|
||||
useInvalidAllLastRun: () => mockInvalidAllLastRun,
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-sandbox-file', () => ({
|
||||
useInvalidateSandboxFiles: () => mockInvalidateSandboxFiles,
|
||||
}))
|
||||
|
||||
vi.mock('../../../hooks', () => ({
|
||||
useWorkflowRun: () => ({
|
||||
handleRun: mockHandleRun,
|
||||
}),
|
||||
useSetWorkflowVarsWithValue: () => ({
|
||||
fetchInspectVars: mockFetchInspectVars,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('../../../hooks-store', () => ({
|
||||
useHooksStore: () => ({
|
||||
configsMap: {
|
||||
flowType: 'workflow',
|
||||
flowId: 'flow-1',
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('../../../store', () => ({
|
||||
useStore: (selector: (state: ChatPreviewStoreState) => unknown) => selector(mockStoreState),
|
||||
useWorkflowStore: () => mockWorkflowStore,
|
||||
}))
|
||||
|
||||
describe('useChatMessageSender HITL regression', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockStoreState.chatTree = [
|
||||
{
|
||||
id: 'question-1',
|
||||
isAnswer: false,
|
||||
children: [
|
||||
{
|
||||
id: 'answer-1',
|
||||
isAnswer: true,
|
||||
content: '',
|
||||
workflowProcess: {
|
||||
status: WorkflowRunningStatus.Running,
|
||||
tracing: [],
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
mockStoreState.updateChatTree = (updater) => {
|
||||
mockStoreState.chatTree = updater(mockStoreState.chatTree)
|
||||
}
|
||||
mockSseGet.mockImplementation(() => undefined)
|
||||
mockWorkflowStore.getState.mockReturnValue({
|
||||
isResponding: false,
|
||||
activeRunId: 1,
|
||||
suggestedQuestionsAbortController: null,
|
||||
workflowEventsAbortController: null,
|
||||
workflowRunningData: {
|
||||
result: {
|
||||
status: WorkflowRunningStatus.Running,
|
||||
},
|
||||
},
|
||||
hasStopResponded: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('should subscribe with include_state_snapshot and re-subscribe on workflow paused event', () => {
|
||||
let callCount = 0
|
||||
mockSseGet.mockImplementation((url: string, _params: unknown, otherOptions: {
|
||||
getAbortController?: (abortController: AbortController) => void
|
||||
onWorkflowPaused?: (event: { data: { workflow_run_id: string } }) => void
|
||||
}) => {
|
||||
callCount += 1
|
||||
otherOptions.getAbortController?.(new AbortController())
|
||||
if (callCount === 1) {
|
||||
otherOptions.onWorkflowPaused?.({
|
||||
data: {
|
||||
workflow_run_id: 'workflow-run-2',
|
||||
},
|
||||
})
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useChatMessageSender({
|
||||
threadMessages: [],
|
||||
config: undefined,
|
||||
formSettings: undefined,
|
||||
handleResponding: vi.fn(),
|
||||
updateCurrentQAOnTree: vi.fn(),
|
||||
}))
|
||||
|
||||
act(() => {
|
||||
result.current.handleResume('answer-1', 'workflow-run-1', {})
|
||||
})
|
||||
|
||||
expect(mockSseGet).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'/workflow/workflow-run-1/events?include_state_snapshot=true',
|
||||
{},
|
||||
expect.any(Object),
|
||||
)
|
||||
expect(mockSseGet).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'/workflow/workflow-run-2/events',
|
||||
{},
|
||||
expect.any(Object),
|
||||
)
|
||||
})
|
||||
|
||||
it('should forward human input form submission to workflow service', async () => {
|
||||
const { result } = renderHook(() => useChatMessageSender({
|
||||
threadMessages: [],
|
||||
config: undefined,
|
||||
formSettings: undefined,
|
||||
handleResponding: vi.fn(),
|
||||
updateCurrentQAOnTree: vi.fn(),
|
||||
}))
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleSubmitHumanInputForm('form-token-1', {
|
||||
inputs: { foo: 'bar' },
|
||||
action: 'submit',
|
||||
})
|
||||
})
|
||||
|
||||
expect(mockSubmitHumanInputForm).toHaveBeenCalledWith('form-token-1', {
|
||||
inputs: { foo: 'bar' },
|
||||
action: 'submit',
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,19 +1,49 @@
|
||||
import type { SendCallback, SendParams, UpdateCurrentQAParams } from './types'
|
||||
import type {
|
||||
SendCallback,
|
||||
SendParams,
|
||||
UpdateCurrentQAParams,
|
||||
} from './types'
|
||||
import type { InputForm } from '@/app/components/base/chat/chat/type'
|
||||
import type { ChatItem, ChatItemInTree, Inputs } from '@/app/components/base/chat/types'
|
||||
import type {
|
||||
ChatItem,
|
||||
ChatItemInTree,
|
||||
Inputs,
|
||||
} from '@/app/components/base/chat/types'
|
||||
import type { IOnDataMoreInfo, IOtherOptions } from '@/service/base'
|
||||
import type {
|
||||
HumanInputFilledFormData,
|
||||
HumanInputFormData,
|
||||
HumanInputFormTimeoutData,
|
||||
NodeTracing,
|
||||
} from '@/types/workflow'
|
||||
import { uniqBy } from 'es-toolkit/compat'
|
||||
import { produce } from 'immer'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { v4 as uuidV4 } from 'uuid'
|
||||
import { getProcessedInputs } from '@/app/components/base/chat/chat/utils'
|
||||
import { getProcessedFiles, getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils'
|
||||
import {
|
||||
getProcessedFiles,
|
||||
getProcessedFilesFromResponse,
|
||||
} from '@/app/components/base/file-uploader/utils'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import {
|
||||
sseGet,
|
||||
} from '@/service/base'
|
||||
import { useInvalidateSandboxFiles } from '@/service/use-sandbox-file'
|
||||
import { useInvalidAllLastRun } from '@/service/use-workflow'
|
||||
import { submitHumanInputForm } from '@/service/workflow'
|
||||
import { TransferMethod } from '@/types/app'
|
||||
import { useSetWorkflowVarsWithValue, useWorkflowRun } from '../../../hooks'
|
||||
import {
|
||||
useSetWorkflowVarsWithValue,
|
||||
useWorkflowRun,
|
||||
} from '../../../hooks'
|
||||
import { useHooksStore } from '../../../hooks-store'
|
||||
import { useStore, useWorkflowStore } from '../../../store'
|
||||
import {
|
||||
NodeRunningStatus,
|
||||
WorkflowRunningStatus,
|
||||
} from '../../../types'
|
||||
import { createWorkflowEventHandlers } from './use-workflow-event-handlers'
|
||||
|
||||
type UseChatMessageSenderParams = {
|
||||
@ -31,6 +61,26 @@ type UseChatMessageSenderParams = {
|
||||
updateCurrentQAOnTree: (params: UpdateCurrentQAParams) => void
|
||||
}
|
||||
|
||||
type StreamChunkMeta = IOnDataMoreInfo
|
||||
|
||||
type WorkflowStartedEvent = {
|
||||
workflow_run_id: string
|
||||
task_id: string
|
||||
conversation_id?: string
|
||||
message_id?: string
|
||||
}
|
||||
|
||||
const getSuggestedQuestionsFromResult = (result: unknown): string[] => {
|
||||
if (!result || typeof result !== 'object' || !('data' in result))
|
||||
return []
|
||||
|
||||
const data = (result as { data: unknown }).data
|
||||
if (!Array.isArray(data))
|
||||
return []
|
||||
|
||||
return data.filter((item): item is string => typeof item === 'string')
|
||||
}
|
||||
|
||||
export function useChatMessageSender({
|
||||
threadMessages,
|
||||
config,
|
||||
@ -47,13 +97,35 @@ export function useChatMessageSender({
|
||||
const invalidAllLastRun = useInvalidAllLastRun(configsMap?.flowType, configsMap?.flowId)
|
||||
const invalidateSandboxFiles = useInvalidateSandboxFiles()
|
||||
const { fetchInspectVars } = useSetWorkflowVarsWithValue()
|
||||
|
||||
const chatTree = useStore(s => s.chatTree)
|
||||
const updateChatTree = useStore(s => s.updateChatTree)
|
||||
const setConversationId = useStore(s => s.setConversationId)
|
||||
const setTargetMessageId = useStore(s => s.setTargetMessageId)
|
||||
const setSuggestedQuestions = useStore(s => s.setSuggestedQuestions)
|
||||
const setActiveTaskId = useStore(s => s.setActiveTaskId)
|
||||
const setHasStopResponded = useStore(s => s.setHasStopResponded)
|
||||
const setSuggestedQuestionsAbortController = useStore(s => s.setSuggestedQuestionsAbortController)
|
||||
const setWorkflowEventsAbortController = useStore(s => s.setWorkflowEventsAbortController)
|
||||
const startRun = useStore(s => s.startRun)
|
||||
|
||||
const updateChatTreeNode = useCallback((messageId: string, updater: (item: ChatItemInTree) => void) => {
|
||||
updateChatTree((currentTree) => {
|
||||
return produce(currentTree, (draft) => {
|
||||
const queue: ChatItemInTree[] = [...draft]
|
||||
while (queue.length > 0) {
|
||||
const current = queue.shift()!
|
||||
if (current.id === messageId) {
|
||||
updater(current)
|
||||
break
|
||||
}
|
||||
if (current.children)
|
||||
queue.push(...current.children)
|
||||
}
|
||||
})
|
||||
})
|
||||
}, [updateChatTree])
|
||||
|
||||
const handleSend = useCallback((
|
||||
params: SendParams,
|
||||
{ onGetSuggestedQuestions }: SendCallback,
|
||||
@ -63,10 +135,18 @@ export function useChatMessageSender({
|
||||
return false
|
||||
}
|
||||
|
||||
const { suggestedQuestionsAbortController } = workflowStore.getState()
|
||||
const {
|
||||
suggestedQuestionsAbortController,
|
||||
workflowEventsAbortController,
|
||||
} = workflowStore.getState()
|
||||
|
||||
if (suggestedQuestionsAbortController)
|
||||
suggestedQuestionsAbortController.abort()
|
||||
if (workflowEventsAbortController)
|
||||
workflowEventsAbortController.abort()
|
||||
|
||||
setSuggestedQuestionsAbortController(null)
|
||||
setWorkflowEventsAbortController(null)
|
||||
|
||||
const runId = startRun()
|
||||
const isCurrentRun = () => runId === workflowStore.getState().activeRunId
|
||||
@ -82,7 +162,7 @@ export function useChatMessageSender({
|
||||
parentMessageId: params.parent_message_id,
|
||||
}
|
||||
|
||||
const siblingIndex = parentMessage?.children?.length ?? workflowStore.getState().chatTree.length
|
||||
const siblingIndex = parentMessage?.children?.length ?? chatTree.length
|
||||
const placeholderAnswerId = `answer-placeholder-${Date.now()}`
|
||||
const placeholderAnswerItem: ChatItem = {
|
||||
id: placeholderAnswerId,
|
||||
@ -108,6 +188,8 @@ export function useChatMessageSender({
|
||||
isAnswer: true,
|
||||
parentMessageId: questionItem.id,
|
||||
siblingIndex,
|
||||
humanInputFormDataList: [],
|
||||
humanInputFilledFormDataList: [],
|
||||
}
|
||||
|
||||
handleResponding(true)
|
||||
@ -118,6 +200,7 @@ export function useChatMessageSender({
|
||||
inputs: getProcessedInputs(inputs || {}, formSettings?.inputsForm || []),
|
||||
...restParams,
|
||||
}
|
||||
|
||||
if (bodyParams?.files?.length) {
|
||||
bodyParams.files = bodyParams.files.map((item) => {
|
||||
if (item.transfer_method === TransferMethod.local_file) {
|
||||
@ -145,6 +228,9 @@ export function useChatMessageSender({
|
||||
handleRun(
|
||||
bodyParams,
|
||||
{
|
||||
getAbortController: (abortController) => {
|
||||
setWorkflowEventsAbortController(abortController)
|
||||
},
|
||||
onData: (message: string, isFirstMessage: boolean, {
|
||||
conversationId: newConversationId,
|
||||
messageId,
|
||||
@ -157,23 +243,23 @@ export function useChatMessageSender({
|
||||
tool_files,
|
||||
tool_error,
|
||||
tool_elapsed_time,
|
||||
}) => {
|
||||
}: StreamChunkMeta) => {
|
||||
if (!isCurrentRun())
|
||||
return
|
||||
if (chunk_type === 'text') {
|
||||
responseItem.content = responseItem.content + message
|
||||
|
||||
if (chunk_type === 'text' || !chunk_type) {
|
||||
responseItem.content = `${responseItem.content}${message}`
|
||||
|
||||
if (!responseItem.llmGenerationItems)
|
||||
responseItem.llmGenerationItems = []
|
||||
|
||||
const isNotCompletedTextItemIndex = responseItem.llmGenerationItems?.findIndex(item => item.type === 'text' && !item.textCompleted)
|
||||
|
||||
const isNotCompletedTextItemIndex = responseItem.llmGenerationItems.findIndex(item => item.type === 'text' && !item.textCompleted)
|
||||
if (isNotCompletedTextItemIndex > -1) {
|
||||
responseItem.llmGenerationItems![isNotCompletedTextItemIndex].text += message
|
||||
responseItem.llmGenerationItems[isNotCompletedTextItemIndex].text += message
|
||||
}
|
||||
else {
|
||||
toolCallId = uuidV4()
|
||||
responseItem.llmGenerationItems?.push({
|
||||
responseItem.llmGenerationItems.push({
|
||||
id: toolCallId,
|
||||
type: 'text',
|
||||
text: message,
|
||||
@ -185,12 +271,12 @@ export function useChatMessageSender({
|
||||
if (!responseItem.llmGenerationItems)
|
||||
responseItem.llmGenerationItems = []
|
||||
|
||||
const isNotCompletedTextItemIndex = responseItem.llmGenerationItems?.findIndex(item => item.type === 'text' && !item.textCompleted)
|
||||
if (isNotCompletedTextItemIndex > -1) {
|
||||
responseItem.llmGenerationItems![isNotCompletedTextItemIndex].textCompleted = true
|
||||
}
|
||||
const isNotCompletedTextItemIndex = responseItem.llmGenerationItems.findIndex(item => item.type === 'text' && !item.textCompleted)
|
||||
if (isNotCompletedTextItemIndex > -1)
|
||||
responseItem.llmGenerationItems[isNotCompletedTextItemIndex].textCompleted = true
|
||||
|
||||
toolCallId = uuidV4()
|
||||
responseItem.llmGenerationItems?.push({
|
||||
responseItem.llmGenerationItems.push({
|
||||
id: toolCallId,
|
||||
type: 'tool',
|
||||
toolName: tool_name,
|
||||
@ -202,7 +288,6 @@ export function useChatMessageSender({
|
||||
|
||||
if (chunk_type === 'tool_result') {
|
||||
const currentToolCallIndex = responseItem.llmGenerationItems?.findIndex(item => item.id === toolCallId) ?? -1
|
||||
|
||||
if (currentToolCallIndex > -1) {
|
||||
responseItem.llmGenerationItems![currentToolCallIndex].toolError = tool_error
|
||||
responseItem.llmGenerationItems![currentToolCallIndex].toolDuration = tool_elapsed_time
|
||||
@ -215,12 +300,12 @@ export function useChatMessageSender({
|
||||
if (!responseItem.llmGenerationItems)
|
||||
responseItem.llmGenerationItems = []
|
||||
|
||||
const isNotCompletedTextItemIndex = responseItem.llmGenerationItems?.findIndex(item => item.type === 'text' && !item.textCompleted)
|
||||
if (isNotCompletedTextItemIndex > -1) {
|
||||
responseItem.llmGenerationItems![isNotCompletedTextItemIndex].textCompleted = true
|
||||
}
|
||||
const isNotCompletedTextItemIndex = responseItem.llmGenerationItems.findIndex(item => item.type === 'text' && !item.textCompleted)
|
||||
if (isNotCompletedTextItemIndex > -1)
|
||||
responseItem.llmGenerationItems[isNotCompletedTextItemIndex].textCompleted = true
|
||||
|
||||
thoughtId = uuidV4()
|
||||
responseItem.llmGenerationItems?.push({
|
||||
responseItem.llmGenerationItems.push({
|
||||
id: thoughtId,
|
||||
type: 'thought',
|
||||
thoughtOutput: '',
|
||||
@ -229,9 +314,8 @@ export function useChatMessageSender({
|
||||
|
||||
if (chunk_type === 'thought') {
|
||||
const currentThoughtIndex = responseItem.llmGenerationItems?.findIndex(item => item.id === thoughtId) ?? -1
|
||||
if (currentThoughtIndex > -1) {
|
||||
if (currentThoughtIndex > -1)
|
||||
responseItem.llmGenerationItems![currentThoughtIndex].thoughtOutput += message
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk_type === 'thought_end') {
|
||||
@ -267,42 +351,48 @@ export function useChatMessageSender({
|
||||
async onCompleted(hasError?: boolean, errorMessage?: string) {
|
||||
if (!isCurrentRun())
|
||||
return
|
||||
|
||||
const { workflowRunningData } = workflowStore.getState()
|
||||
handleResponding(false)
|
||||
fetchInspectVars({})
|
||||
invalidAllLastRun()
|
||||
invalidateSandboxFiles()
|
||||
setWorkflowEventsAbortController(null)
|
||||
|
||||
if (hasError) {
|
||||
if (errorMessage) {
|
||||
responseItem.content = errorMessage
|
||||
responseItem.isError = true
|
||||
responseItem.llmGenerationItems?.forEach((item) => {
|
||||
if (item.type === 'text')
|
||||
item.isError = true
|
||||
})
|
||||
updateCurrentQAOnTree({
|
||||
placeholderQuestionId,
|
||||
questionItem,
|
||||
responseItem,
|
||||
parentId: params.parent_message_id,
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
if (workflowRunningData?.result.status !== WorkflowRunningStatus.Paused) {
|
||||
fetchInspectVars({})
|
||||
invalidAllLastRun()
|
||||
invalidateSandboxFiles()
|
||||
|
||||
if (config?.suggested_questions_after_answer?.enabled && !workflowStore.getState().hasStopResponded && onGetSuggestedQuestions) {
|
||||
try {
|
||||
const result = await onGetSuggestedQuestions(
|
||||
responseItem.id,
|
||||
newAbortController => setSuggestedQuestionsAbortController(newAbortController),
|
||||
) as { data: string[] }
|
||||
setSuggestedQuestions(result.data)
|
||||
if (hasError) {
|
||||
if (errorMessage) {
|
||||
responseItem.content = errorMessage
|
||||
responseItem.isError = true
|
||||
responseItem.llmGenerationItems?.forEach((item) => {
|
||||
if (item.type === 'text')
|
||||
item.isError = true
|
||||
})
|
||||
updateCurrentQAOnTree({
|
||||
placeholderQuestionId,
|
||||
questionItem,
|
||||
responseItem,
|
||||
parentId: params.parent_message_id,
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
catch {
|
||||
setSuggestedQuestions([])
|
||||
}
|
||||
finally {
|
||||
setSuggestedQuestionsAbortController(null)
|
||||
|
||||
if (config?.suggested_questions_after_answer?.enabled && !workflowStore.getState().hasStopResponded && onGetSuggestedQuestions) {
|
||||
try {
|
||||
const result = await onGetSuggestedQuestions(
|
||||
responseItem.id,
|
||||
newAbortController => setSuggestedQuestionsAbortController(newAbortController),
|
||||
)
|
||||
setSuggestedQuestions(getSuggestedQuestionsFromResult(result))
|
||||
}
|
||||
catch {
|
||||
setSuggestedQuestions([])
|
||||
}
|
||||
finally {
|
||||
setSuggestedQuestionsAbortController(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -329,11 +419,24 @@ export function useChatMessageSender({
|
||||
if (!isCurrentRun())
|
||||
return
|
||||
handleResponding(false)
|
||||
setWorkflowEventsAbortController(null)
|
||||
},
|
||||
onWorkflowStarted: (event) => {
|
||||
if (!isCurrentRun())
|
||||
return
|
||||
const taskId = workflowHandlers.onWorkflowStarted(event)
|
||||
|
||||
const workflowStartEvent = event as WorkflowStartedEvent
|
||||
if (workflowStartEvent.conversation_id)
|
||||
setConversationId(workflowStartEvent.conversation_id)
|
||||
|
||||
if (workflowStartEvent.message_id && !hasSetResponseId) {
|
||||
questionItem.id = `question-${workflowStartEvent.message_id}`
|
||||
responseItem.id = workflowStartEvent.message_id
|
||||
responseItem.parentMessageId = questionItem.id
|
||||
hasSetResponseId = true
|
||||
}
|
||||
|
||||
const taskId = workflowHandlers.onWorkflowStarted(workflowStartEvent)
|
||||
if (taskId)
|
||||
setActiveTaskId(taskId)
|
||||
},
|
||||
@ -382,28 +485,430 @@ export function useChatMessageSender({
|
||||
return
|
||||
workflowHandlers.onAgentLog(event)
|
||||
},
|
||||
onHumanInputRequired: (event) => {
|
||||
if (!isCurrentRun())
|
||||
return
|
||||
workflowHandlers.onHumanInputRequired(event)
|
||||
},
|
||||
onHumanInputFormFilled: (event) => {
|
||||
if (!isCurrentRun())
|
||||
return
|
||||
workflowHandlers.onHumanInputFormFilled(event)
|
||||
},
|
||||
onHumanInputFormTimeout: (event) => {
|
||||
if (!isCurrentRun())
|
||||
return
|
||||
workflowHandlers.onHumanInputFormTimeout(event)
|
||||
},
|
||||
onWorkflowPaused: () => {
|
||||
if (!isCurrentRun())
|
||||
return
|
||||
workflowHandlers.onWorkflowPaused()
|
||||
},
|
||||
},
|
||||
)
|
||||
return true
|
||||
}, [
|
||||
workflowStore,
|
||||
notify,
|
||||
t,
|
||||
setSuggestedQuestionsAbortController,
|
||||
setWorkflowEventsAbortController,
|
||||
startRun,
|
||||
threadMessages,
|
||||
chatTree.length,
|
||||
setTargetMessageId,
|
||||
updateCurrentQAOnTree,
|
||||
handleResponding,
|
||||
formSettings?.inputsForm,
|
||||
handleRun,
|
||||
notify,
|
||||
t,
|
||||
config?.suggested_questions_after_answer?.enabled,
|
||||
setTargetMessageId,
|
||||
setConversationId,
|
||||
setSuggestedQuestions,
|
||||
setActiveTaskId,
|
||||
setSuggestedQuestionsAbortController,
|
||||
startRun,
|
||||
fetchInspectVars,
|
||||
invalidAllLastRun,
|
||||
invalidateSandboxFiles,
|
||||
workflowStore,
|
||||
config?.suggested_questions_after_answer?.enabled,
|
||||
setSuggestedQuestions,
|
||||
])
|
||||
|
||||
return { handleSend }
|
||||
const handleSubmitHumanInputForm = useCallback(async (formToken: string, formData: {
|
||||
inputs: Record<string, string>
|
||||
action: string
|
||||
}) => {
|
||||
await submitHumanInputForm(formToken, formData)
|
||||
}, [])
|
||||
|
||||
const handleResume = useCallback((
|
||||
messageId: string,
|
||||
workflowRunId: string,
|
||||
{
|
||||
onGetSuggestedQuestions,
|
||||
}: SendCallback,
|
||||
) => {
|
||||
const url = `/workflow/${workflowRunId}/events?include_state_snapshot=true`
|
||||
let toolCallId = ''
|
||||
let thoughtId = ''
|
||||
|
||||
const otherOptions: IOtherOptions = {
|
||||
getAbortController: (abortController) => {
|
||||
setWorkflowEventsAbortController(abortController)
|
||||
},
|
||||
onData: (message: string, _isFirstMessage: boolean, {
|
||||
conversationId: newConversationId,
|
||||
messageId: msgId,
|
||||
taskId,
|
||||
chunk_type,
|
||||
tool_icon,
|
||||
tool_icon_dark,
|
||||
tool_name,
|
||||
tool_arguments,
|
||||
tool_files,
|
||||
tool_error,
|
||||
tool_elapsed_time,
|
||||
}: StreamChunkMeta) => {
|
||||
updateChatTreeNode(messageId, (responseItem) => {
|
||||
if (chunk_type === 'text' || !chunk_type) {
|
||||
responseItem.content = `${responseItem.content}${message}`
|
||||
if (!responseItem.llmGenerationItems)
|
||||
responseItem.llmGenerationItems = []
|
||||
|
||||
const isNotCompletedTextItemIndex = responseItem.llmGenerationItems.findIndex(item => item.type === 'text' && !item.textCompleted)
|
||||
if (isNotCompletedTextItemIndex > -1) {
|
||||
responseItem.llmGenerationItems[isNotCompletedTextItemIndex].text += message
|
||||
}
|
||||
else {
|
||||
toolCallId = uuidV4()
|
||||
responseItem.llmGenerationItems.push({
|
||||
id: toolCallId,
|
||||
type: 'text',
|
||||
text: message,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk_type === 'tool_call') {
|
||||
if (!responseItem.llmGenerationItems)
|
||||
responseItem.llmGenerationItems = []
|
||||
|
||||
const isNotCompletedTextItemIndex = responseItem.llmGenerationItems.findIndex(item => item.type === 'text' && !item.textCompleted)
|
||||
if (isNotCompletedTextItemIndex > -1)
|
||||
responseItem.llmGenerationItems[isNotCompletedTextItemIndex].textCompleted = true
|
||||
|
||||
toolCallId = uuidV4()
|
||||
responseItem.llmGenerationItems.push({
|
||||
id: toolCallId,
|
||||
type: 'tool',
|
||||
toolName: tool_name,
|
||||
toolArguments: tool_arguments,
|
||||
toolIcon: tool_icon,
|
||||
toolIconDark: tool_icon_dark,
|
||||
})
|
||||
}
|
||||
|
||||
if (chunk_type === 'tool_result') {
|
||||
const currentToolCallIndex = responseItem.llmGenerationItems?.findIndex(item => item.id === toolCallId) ?? -1
|
||||
if (currentToolCallIndex > -1) {
|
||||
responseItem.llmGenerationItems![currentToolCallIndex].toolError = tool_error
|
||||
responseItem.llmGenerationItems![currentToolCallIndex].toolDuration = tool_elapsed_time
|
||||
responseItem.llmGenerationItems![currentToolCallIndex].toolFiles = tool_files
|
||||
responseItem.llmGenerationItems![currentToolCallIndex].toolOutput = message
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk_type === 'thought_start') {
|
||||
if (!responseItem.llmGenerationItems)
|
||||
responseItem.llmGenerationItems = []
|
||||
|
||||
const isNotCompletedTextItemIndex = responseItem.llmGenerationItems.findIndex(item => item.type === 'text' && !item.textCompleted)
|
||||
if (isNotCompletedTextItemIndex > -1)
|
||||
responseItem.llmGenerationItems[isNotCompletedTextItemIndex].textCompleted = true
|
||||
|
||||
thoughtId = uuidV4()
|
||||
responseItem.llmGenerationItems.push({
|
||||
id: thoughtId,
|
||||
type: 'thought',
|
||||
thoughtOutput: '',
|
||||
})
|
||||
}
|
||||
|
||||
if (chunk_type === 'thought') {
|
||||
const currentThoughtIndex = responseItem.llmGenerationItems?.findIndex(item => item.id === thoughtId) ?? -1
|
||||
if (currentThoughtIndex > -1)
|
||||
responseItem.llmGenerationItems![currentThoughtIndex].thoughtOutput += message
|
||||
}
|
||||
|
||||
if (chunk_type === 'thought_end') {
|
||||
const currentThoughtIndex = responseItem.llmGenerationItems?.findIndex(item => item.id === thoughtId) ?? -1
|
||||
if (currentThoughtIndex > -1) {
|
||||
responseItem.llmGenerationItems![currentThoughtIndex].thoughtOutput += message
|
||||
responseItem.llmGenerationItems![currentThoughtIndex].thoughtCompleted = true
|
||||
}
|
||||
}
|
||||
|
||||
if (msgId)
|
||||
responseItem.id = msgId
|
||||
})
|
||||
|
||||
if (newConversationId)
|
||||
setConversationId(newConversationId)
|
||||
if (taskId)
|
||||
setActiveTaskId(taskId)
|
||||
},
|
||||
async onCompleted(hasError?: boolean) {
|
||||
const { workflowRunningData, hasStopResponded } = workflowStore.getState()
|
||||
handleResponding(false)
|
||||
setWorkflowEventsAbortController(null)
|
||||
|
||||
if (workflowRunningData?.result.status !== WorkflowRunningStatus.Paused) {
|
||||
fetchInspectVars({})
|
||||
invalidAllLastRun()
|
||||
invalidateSandboxFiles()
|
||||
|
||||
if (hasError)
|
||||
return
|
||||
|
||||
if (config?.suggested_questions_after_answer?.enabled && !hasStopResponded && onGetSuggestedQuestions) {
|
||||
try {
|
||||
const result = await onGetSuggestedQuestions(
|
||||
messageId,
|
||||
newAbortController => setSuggestedQuestionsAbortController(newAbortController),
|
||||
)
|
||||
setSuggestedQuestions(getSuggestedQuestionsFromResult(result))
|
||||
}
|
||||
catch {
|
||||
setSuggestedQuestions([])
|
||||
}
|
||||
finally {
|
||||
setSuggestedQuestionsAbortController(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onMessageEnd: (messageEnd) => {
|
||||
updateChatTreeNode(messageId, (responseItem) => {
|
||||
responseItem.citation = messageEnd.metadata?.retriever_resources || []
|
||||
const processedFilesFromResponse = getProcessedFilesFromResponse(messageEnd.files || [])
|
||||
responseItem.allFiles = uniqBy([...(responseItem.allFiles || []), ...(processedFilesFromResponse || [])], 'id')
|
||||
})
|
||||
},
|
||||
onMessageReplace: (messageReplace) => {
|
||||
updateChatTreeNode(messageId, (responseItem) => {
|
||||
responseItem.content = messageReplace.answer
|
||||
})
|
||||
},
|
||||
onError() {
|
||||
handleResponding(false)
|
||||
setWorkflowEventsAbortController(null)
|
||||
},
|
||||
onWorkflowStarted: ({ workflow_run_id, task_id }: WorkflowStartedEvent) => {
|
||||
handleResponding(true)
|
||||
setHasStopResponded(false)
|
||||
updateChatTreeNode(messageId, (responseItem) => {
|
||||
if (responseItem.workflowProcess && responseItem.workflowProcess.tracing.length > 0) {
|
||||
responseItem.workflowProcess.status = WorkflowRunningStatus.Running
|
||||
}
|
||||
else {
|
||||
if (task_id)
|
||||
setActiveTaskId(task_id)
|
||||
responseItem.workflow_run_id = workflow_run_id
|
||||
responseItem.workflowProcess = {
|
||||
status: WorkflowRunningStatus.Running,
|
||||
tracing: [],
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
onWorkflowFinished: ({ data: workflowFinishedData }) => {
|
||||
updateChatTreeNode(messageId, (responseItem) => {
|
||||
if (responseItem.workflowProcess)
|
||||
responseItem.workflowProcess.status = workflowFinishedData.status as WorkflowRunningStatus
|
||||
})
|
||||
},
|
||||
onIterationStart: ({ data: iterationStartedData }) => {
|
||||
updateChatTreeNode(messageId, (responseItem) => {
|
||||
if (!responseItem.workflowProcess)
|
||||
return
|
||||
if (!responseItem.workflowProcess.tracing)
|
||||
responseItem.workflowProcess.tracing = []
|
||||
responseItem.workflowProcess.tracing.push({
|
||||
...iterationStartedData,
|
||||
status: WorkflowRunningStatus.Running,
|
||||
} as NodeTracing)
|
||||
})
|
||||
},
|
||||
onIterationFinish: ({ data: iterationFinishedData }) => {
|
||||
updateChatTreeNode(messageId, (responseItem) => {
|
||||
if (!responseItem.workflowProcess?.tracing)
|
||||
return
|
||||
const tracing = responseItem.workflowProcess.tracing
|
||||
const iterationIndex = tracing.findIndex(item => item.node_id === iterationFinishedData.node_id
|
||||
&& (item.execution_metadata?.parallel_id === iterationFinishedData.execution_metadata?.parallel_id || item.parallel_id === iterationFinishedData.execution_metadata?.parallel_id))
|
||||
if (iterationIndex > -1) {
|
||||
tracing[iterationIndex] = {
|
||||
...tracing[iterationIndex],
|
||||
...iterationFinishedData,
|
||||
status: WorkflowRunningStatus.Succeeded,
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
onNodeStarted: ({ data: nodeStartedData }) => {
|
||||
updateChatTreeNode(messageId, (responseItem) => {
|
||||
if (!responseItem.workflowProcess)
|
||||
return
|
||||
if (!responseItem.workflowProcess.tracing)
|
||||
responseItem.workflowProcess.tracing = []
|
||||
|
||||
const currentIndex = responseItem.workflowProcess.tracing.findIndex(item => item.node_id === nodeStartedData.node_id)
|
||||
if (currentIndex > -1) {
|
||||
responseItem.workflowProcess.tracing[currentIndex] = {
|
||||
...nodeStartedData,
|
||||
status: NodeRunningStatus.Running,
|
||||
} as NodeTracing
|
||||
}
|
||||
else {
|
||||
if (nodeStartedData.iteration_id)
|
||||
return
|
||||
|
||||
responseItem.workflowProcess.tracing.push({
|
||||
...nodeStartedData,
|
||||
status: WorkflowRunningStatus.Running,
|
||||
} as NodeTracing)
|
||||
}
|
||||
})
|
||||
},
|
||||
onNodeFinished: ({ data: nodeFinishedData }) => {
|
||||
updateChatTreeNode(messageId, (responseItem) => {
|
||||
if (!responseItem.workflowProcess?.tracing)
|
||||
return
|
||||
if (nodeFinishedData.iteration_id)
|
||||
return
|
||||
|
||||
const currentIndex = responseItem.workflowProcess.tracing.findIndex((item) => {
|
||||
if (!item.execution_metadata?.parallel_id)
|
||||
return item.id === nodeFinishedData.id
|
||||
|
||||
return item.id === nodeFinishedData.id && item.execution_metadata.parallel_id === nodeFinishedData.execution_metadata?.parallel_id
|
||||
})
|
||||
|
||||
if (currentIndex > -1)
|
||||
responseItem.workflowProcess.tracing[currentIndex] = nodeFinishedData as NodeTracing
|
||||
})
|
||||
},
|
||||
onLoopStart: ({ data: loopStartedData }) => {
|
||||
updateChatTreeNode(messageId, (responseItem) => {
|
||||
if (!responseItem.workflowProcess)
|
||||
return
|
||||
if (!responseItem.workflowProcess.tracing)
|
||||
responseItem.workflowProcess.tracing = []
|
||||
responseItem.workflowProcess.tracing.push({
|
||||
...loopStartedData,
|
||||
status: WorkflowRunningStatus.Running,
|
||||
} as NodeTracing)
|
||||
})
|
||||
},
|
||||
onLoopFinish: ({ data: loopFinishedData }) => {
|
||||
updateChatTreeNode(messageId, (responseItem) => {
|
||||
if (!responseItem.workflowProcess?.tracing)
|
||||
return
|
||||
const tracing = responseItem.workflowProcess.tracing
|
||||
const loopIndex = tracing.findIndex(item => item.node_id === loopFinishedData.node_id
|
||||
&& (item.execution_metadata?.parallel_id === loopFinishedData.execution_metadata?.parallel_id || item.parallel_id === loopFinishedData.execution_metadata?.parallel_id))
|
||||
if (loopIndex > -1) {
|
||||
tracing[loopIndex] = {
|
||||
...tracing[loopIndex],
|
||||
...loopFinishedData,
|
||||
status: WorkflowRunningStatus.Succeeded,
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
onHumanInputRequired: ({ data: humanInputRequiredData }: { data: HumanInputFormData }) => {
|
||||
updateChatTreeNode(messageId, (responseItem) => {
|
||||
if (!responseItem.humanInputFormDataList) {
|
||||
responseItem.humanInputFormDataList = [humanInputRequiredData]
|
||||
}
|
||||
else {
|
||||
const currentFormIndex = responseItem.humanInputFormDataList.findIndex(item => item.node_id === humanInputRequiredData.node_id)
|
||||
if (currentFormIndex > -1)
|
||||
responseItem.humanInputFormDataList[currentFormIndex] = humanInputRequiredData
|
||||
else
|
||||
responseItem.humanInputFormDataList.push(humanInputRequiredData)
|
||||
}
|
||||
|
||||
if (responseItem.workflowProcess?.tracing) {
|
||||
const currentTracingIndex = responseItem.workflowProcess.tracing.findIndex(item => item.node_id === humanInputRequiredData.node_id)
|
||||
if (currentTracingIndex > -1)
|
||||
responseItem.workflowProcess.tracing[currentTracingIndex].status = NodeRunningStatus.Paused
|
||||
}
|
||||
})
|
||||
},
|
||||
onHumanInputFormFilled: ({ data: humanInputFilledFormData }: { data: HumanInputFilledFormData }) => {
|
||||
updateChatTreeNode(messageId, (responseItem) => {
|
||||
if (responseItem.humanInputFormDataList?.length) {
|
||||
const currentFormIndex = responseItem.humanInputFormDataList.findIndex(item => item.node_id === humanInputFilledFormData.node_id)
|
||||
if (currentFormIndex > -1)
|
||||
responseItem.humanInputFormDataList.splice(currentFormIndex, 1)
|
||||
}
|
||||
if (!responseItem.humanInputFilledFormDataList)
|
||||
responseItem.humanInputFilledFormDataList = [humanInputFilledFormData]
|
||||
else
|
||||
responseItem.humanInputFilledFormDataList.push(humanInputFilledFormData)
|
||||
})
|
||||
},
|
||||
onHumanInputFormTimeout: ({ data: humanInputFormTimeoutData }: { data: HumanInputFormTimeoutData }) => {
|
||||
updateChatTreeNode(messageId, (responseItem) => {
|
||||
if (responseItem.humanInputFormDataList?.length) {
|
||||
const currentFormIndex = responseItem.humanInputFormDataList.findIndex(item => item.node_id === humanInputFormTimeoutData.node_id)
|
||||
if (currentFormIndex > -1)
|
||||
responseItem.humanInputFormDataList[currentFormIndex].expiration_time = humanInputFormTimeoutData.expiration_time
|
||||
}
|
||||
})
|
||||
},
|
||||
onWorkflowPaused: ({ data: workflowPausedData }: { data: { workflow_run_id: string } }) => {
|
||||
const resumeUrl = `/workflow/${workflowPausedData.workflow_run_id}/events`
|
||||
sseGet(
|
||||
resumeUrl,
|
||||
{},
|
||||
otherOptions,
|
||||
)
|
||||
updateChatTreeNode(messageId, (responseItem) => {
|
||||
if (responseItem.workflowProcess)
|
||||
responseItem.workflowProcess.status = WorkflowRunningStatus.Paused
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
const { workflowEventsAbortController } = workflowStore.getState()
|
||||
if (workflowEventsAbortController)
|
||||
workflowEventsAbortController.abort()
|
||||
|
||||
setWorkflowEventsAbortController(null)
|
||||
|
||||
sseGet(
|
||||
url,
|
||||
{},
|
||||
otherOptions,
|
||||
)
|
||||
}, [
|
||||
workflowStore,
|
||||
setWorkflowEventsAbortController,
|
||||
updateChatTreeNode,
|
||||
setConversationId,
|
||||
setActiveTaskId,
|
||||
handleResponding,
|
||||
setHasStopResponded,
|
||||
fetchInspectVars,
|
||||
invalidAllLastRun,
|
||||
invalidateSandboxFiles,
|
||||
config?.suggested_questions_after_answer?.enabled,
|
||||
setSuggestedQuestions,
|
||||
setSuggestedQuestionsAbortController,
|
||||
])
|
||||
|
||||
return {
|
||||
handleSend,
|
||||
handleResume,
|
||||
handleSubmitHumanInputForm,
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,136 @@
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import { vi } from 'vitest'
|
||||
import { CUSTOM_NODE } from '../../../constants'
|
||||
import { useChat } from './use-chat'
|
||||
|
||||
const {
|
||||
mockHandleSend,
|
||||
mockHandleResume,
|
||||
mockHandleSubmitHumanInputForm,
|
||||
mockGetNodes,
|
||||
} = vi.hoisted(() => ({
|
||||
mockHandleSend: vi.fn(),
|
||||
mockHandleResume: vi.fn(),
|
||||
mockHandleSubmitHumanInputForm: vi.fn(),
|
||||
mockGetNodes: vi.fn(),
|
||||
}))
|
||||
|
||||
type ChatTreeNode = {
|
||||
id: string
|
||||
children?: ChatTreeNode[]
|
||||
workflow_run_id?: string
|
||||
humanInputFormDataList?: Array<{ node_id: string }>
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
type StoreState = {
|
||||
chatTree: ChatTreeNode[]
|
||||
conversationId: string
|
||||
isResponding: boolean
|
||||
suggestedQuestions: string[]
|
||||
targetMessageId?: string
|
||||
updateChatTree: (updater: (current: ChatTreeNode[]) => ChatTreeNode[]) => void
|
||||
setTargetMessageId: ReturnType<typeof vi.fn>
|
||||
}
|
||||
|
||||
const mockStoreState: StoreState = {
|
||||
chatTree: [],
|
||||
conversationId: 'conversation-1',
|
||||
isResponding: false,
|
||||
suggestedQuestions: [],
|
||||
targetMessageId: undefined,
|
||||
updateChatTree: () => {},
|
||||
setTargetMessageId: vi.fn(),
|
||||
}
|
||||
|
||||
vi.mock('reactflow', () => ({
|
||||
useStoreApi: () => ({
|
||||
getState: () => ({
|
||||
getNodes: mockGetNodes,
|
||||
}),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('../../../store', () => ({
|
||||
useStore: (selector: (state: StoreState) => unknown) => selector(mockStoreState),
|
||||
}))
|
||||
|
||||
vi.mock('./use-chat-flow-control', () => ({
|
||||
useChatFlowControl: () => ({
|
||||
handleResponding: vi.fn(),
|
||||
handleStop: vi.fn(),
|
||||
handleRestart: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('./use-chat-list', () => ({
|
||||
useChatList: () => ({
|
||||
threadMessages: [],
|
||||
chatList: [],
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('./use-chat-tree-operations', () => ({
|
||||
useChatTreeOperations: () => ({
|
||||
updateCurrentQAOnTree: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('./use-chat-message-sender', () => ({
|
||||
useChatMessageSender: () => ({
|
||||
handleSend: mockHandleSend,
|
||||
handleResume: mockHandleResume,
|
||||
handleSubmitHumanInputForm: mockHandleSubmitHumanInputForm,
|
||||
}),
|
||||
}))
|
||||
|
||||
describe('useChat (debug-and-preview)', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockStoreState.chatTree = []
|
||||
mockStoreState.conversationId = 'conversation-1'
|
||||
mockStoreState.isResponding = false
|
||||
mockStoreState.suggestedQuestions = []
|
||||
mockStoreState.targetMessageId = undefined
|
||||
mockGetNodes.mockReturnValue([
|
||||
{ id: 'custom-node-1', type: CUSTOM_NODE, data: { title: 'Node 1' } },
|
||||
{ id: 'other-node-1', type: 'input', data: { title: 'Input' } },
|
||||
])
|
||||
})
|
||||
|
||||
it('should call handleResume when switching to sibling with pending human input form', () => {
|
||||
mockStoreState.chatTree = [
|
||||
{
|
||||
id: 'question-1',
|
||||
isAnswer: false,
|
||||
children: [
|
||||
{
|
||||
id: 'answer-1',
|
||||
isAnswer: true,
|
||||
workflow_run_id: 'workflow-run-1',
|
||||
humanInputFormDataList: [{ node_id: 'node-1' }],
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const { result } = renderHook(() => useChat(undefined, undefined))
|
||||
|
||||
const callbacks = { onGetSuggestedQuestions: vi.fn() }
|
||||
act(() => {
|
||||
result.current.handleSwitchSibling('answer-1', callbacks)
|
||||
})
|
||||
|
||||
expect(mockStoreState.setTargetMessageId).toHaveBeenCalledWith('answer-1')
|
||||
expect(mockHandleResume).toHaveBeenCalledWith('answer-1', 'workflow-run-1', callbacks)
|
||||
})
|
||||
|
||||
it('should expose getHumanInputNodeData from reactflow nodes', () => {
|
||||
const { result } = renderHook(() => useChat(undefined, undefined))
|
||||
|
||||
const node = result.current.getHumanInputNodeData('custom-node-1')
|
||||
|
||||
expect(node).toEqual({ id: 'custom-node-1', type: CUSTOM_NODE, data: { title: 'Node 1' } })
|
||||
})
|
||||
})
|
||||
@ -1,7 +1,9 @@
|
||||
import type { ChatConfig } from './types'
|
||||
import type { ChatConfig, SendCallback } from './types'
|
||||
import type { InputForm } from '@/app/components/base/chat/chat/type'
|
||||
import type { ChatItemInTree, Inputs } from '@/app/components/base/chat/types'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useCallback, useEffect, useRef } from 'react'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import { CUSTOM_NODE } from '../../../constants'
|
||||
import { useStore } from '../../../store'
|
||||
import { useChatFlowControl } from './use-chat-flow-control'
|
||||
import { useChatList } from './use-chat-list'
|
||||
@ -24,6 +26,7 @@ export function useChat(
|
||||
const targetMessageId = useStore(s => s.targetMessageId)
|
||||
const updateChatTree = useStore(s => s.updateChatTree)
|
||||
const setTargetMessageId = useStore(s => s.setTargetMessageId)
|
||||
const store = useStoreApi()
|
||||
|
||||
const initialChatTreeRef = useRef(prevChatTree)
|
||||
useEffect(() => {
|
||||
@ -53,7 +56,11 @@ export function useChat(
|
||||
formSettings,
|
||||
})
|
||||
|
||||
const { handleSend } = useChatMessageSender({
|
||||
const {
|
||||
handleSend,
|
||||
handleResume,
|
||||
handleSubmitHumanInputForm,
|
||||
} = useChatMessageSender({
|
||||
threadMessages,
|
||||
config,
|
||||
formSettings,
|
||||
@ -61,11 +68,49 @@ export function useChat(
|
||||
updateCurrentQAOnTree,
|
||||
})
|
||||
|
||||
const getHumanInputNodeData = useCallback((nodeID: string) => {
|
||||
const { getNodes } = store.getState()
|
||||
const nodes = getNodes().filter(node => node.type === CUSTOM_NODE)
|
||||
return nodes.find(node => node.id === nodeID)
|
||||
}, [store])
|
||||
|
||||
const handleSwitchSibling = useCallback((
|
||||
siblingMessageId: string,
|
||||
callbacks: SendCallback,
|
||||
) => {
|
||||
setTargetMessageId(siblingMessageId)
|
||||
|
||||
const findMessageInTree = (nodes: ChatItemInTree[], targetId: string): ChatItemInTree | undefined => {
|
||||
for (const node of nodes) {
|
||||
if (node.id === targetId)
|
||||
return node
|
||||
if (node.children) {
|
||||
const found = findMessageInTree(node.children, targetId)
|
||||
if (found)
|
||||
return found
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
const targetMessage = findMessageInTree(chatTree, siblingMessageId)
|
||||
if (targetMessage?.workflow_run_id && targetMessage.humanInputFormDataList?.length) {
|
||||
handleResume(
|
||||
targetMessage.id,
|
||||
targetMessage.workflow_run_id,
|
||||
callbacks,
|
||||
)
|
||||
}
|
||||
}, [chatTree, handleResume, setTargetMessageId])
|
||||
|
||||
return {
|
||||
conversationId,
|
||||
chatList,
|
||||
setTargetMessageId,
|
||||
handleSend,
|
||||
handleSwitchSibling,
|
||||
handleSubmitHumanInputForm,
|
||||
getHumanInputNodeData,
|
||||
handleStop,
|
||||
handleRestart,
|
||||
isResponding,
|
||||
|
||||
@ -1,6 +1,12 @@
|
||||
import type { UpdateCurrentQAParams } from './types'
|
||||
import type { ChatItem } from '@/app/components/base/chat/types'
|
||||
import type { AgentLogItem, NodeTracing } from '@/types/workflow'
|
||||
import type {
|
||||
AgentLogItem,
|
||||
HumanInputFilledFormData,
|
||||
HumanInputFormData,
|
||||
HumanInputFormTimeoutData,
|
||||
NodeTracing,
|
||||
} from '@/types/workflow'
|
||||
import { NodeRunningStatus, WorkflowRunningStatus } from '../../../types'
|
||||
|
||||
type WorkflowEventHandlersContext = {
|
||||
@ -94,6 +100,56 @@ export function createWorkflowEventHandlers(ctx: WorkflowEventHandlersContext) {
|
||||
updateTracingItem(data)
|
||||
},
|
||||
|
||||
onHumanInputRequired: ({ data }: { data: HumanInputFormData }) => {
|
||||
if (!responseItem.humanInputFormDataList) {
|
||||
responseItem.humanInputFormDataList = [data]
|
||||
}
|
||||
else {
|
||||
const currentFormIndex = responseItem.humanInputFormDataList.findIndex(item => item.node_id === data.node_id)
|
||||
if (currentFormIndex > -1)
|
||||
responseItem.humanInputFormDataList[currentFormIndex] = data
|
||||
else
|
||||
responseItem.humanInputFormDataList.push(data)
|
||||
}
|
||||
|
||||
const currentTracingIndex = responseItem.workflowProcess?.tracing?.findIndex(item => item.node_id === data.node_id) ?? -1
|
||||
if (currentTracingIndex > -1) {
|
||||
responseItem.workflowProcess!.tracing[currentTracingIndex].status = NodeRunningStatus.Paused
|
||||
}
|
||||
|
||||
updateTree()
|
||||
},
|
||||
|
||||
onHumanInputFormFilled: ({ data }: { data: HumanInputFilledFormData }) => {
|
||||
if (responseItem.humanInputFormDataList?.length) {
|
||||
const currentFormIndex = responseItem.humanInputFormDataList.findIndex(item => item.node_id === data.node_id)
|
||||
if (currentFormIndex > -1)
|
||||
responseItem.humanInputFormDataList.splice(currentFormIndex, 1)
|
||||
}
|
||||
|
||||
if (!responseItem.humanInputFilledFormDataList)
|
||||
responseItem.humanInputFilledFormDataList = [data]
|
||||
else
|
||||
responseItem.humanInputFilledFormDataList.push(data)
|
||||
|
||||
updateTree()
|
||||
},
|
||||
|
||||
onHumanInputFormTimeout: ({ data }: { data: HumanInputFormTimeoutData }) => {
|
||||
if (responseItem.humanInputFormDataList?.length) {
|
||||
const currentFormIndex = responseItem.humanInputFormDataList.findIndex(item => item.node_id === data.node_id)
|
||||
if (currentFormIndex > -1)
|
||||
responseItem.humanInputFormDataList[currentFormIndex].expiration_time = data.expiration_time
|
||||
}
|
||||
updateTree()
|
||||
},
|
||||
|
||||
onWorkflowPaused: () => {
|
||||
if (responseItem.workflowProcess)
|
||||
responseItem.workflowProcess.status = WorkflowRunningStatus.Paused
|
||||
updateTree()
|
||||
},
|
||||
|
||||
onAgentLog: ({ data }: { data: AgentLogData }) => {
|
||||
const currentNodeIndex = responseItem.workflowProcess!.tracing!.findIndex(item => item.node_id === data.node_id)
|
||||
if (currentNodeIndex > -1) {
|
||||
|
||||
@ -0,0 +1,34 @@
|
||||
import type { HumanInputFilledFormData } from '@/types/workflow'
|
||||
import ContentWrapper from '@/app/components/base/chat/chat/answer/human-input-content/content-wrapper'
|
||||
import { SubmittedHumanInputContent } from '@/app/components/base/chat/chat/answer/human-input-content/submitted'
|
||||
|
||||
type HumanInputFilledFormListProps = {
|
||||
humanInputFilledFormDataList: HumanInputFilledFormData[]
|
||||
}
|
||||
|
||||
const HumanInputFilledFormList = ({
|
||||
humanInputFilledFormDataList,
|
||||
}: HumanInputFilledFormListProps) => {
|
||||
return (
|
||||
<div className="mt-3 flex flex-col gap-y-3 first:mt-0">
|
||||
{
|
||||
humanInputFilledFormDataList.map(formData => (
|
||||
<ContentWrapper
|
||||
key={formData.node_id}
|
||||
nodeTitle={formData.node_title}
|
||||
showExpandIcon
|
||||
className="bg-components-panel-bg"
|
||||
expanded
|
||||
>
|
||||
<SubmittedHumanInputContent
|
||||
key={formData.node_id}
|
||||
formData={formData}
|
||||
/>
|
||||
</ContentWrapper>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default HumanInputFilledFormList
|
||||
83
web/app/components/workflow/panel/human-input-form-list.tsx
Normal file
83
web/app/components/workflow/panel/human-input-form-list.tsx
Normal file
@ -0,0 +1,83 @@
|
||||
import type { DeliveryMethod } from '@/app/components/workflow/nodes/human-input/types'
|
||||
import type { HumanInputFormData } from '@/types/workflow'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import ContentWrapper from '@/app/components/base/chat/chat/answer/human-input-content/content-wrapper'
|
||||
import { UnsubmittedHumanInputContent } from '@/app/components/base/chat/chat/answer/human-input-content/unsubmitted'
|
||||
import { CUSTOM_NODE } from '@/app/components/workflow/constants'
|
||||
import { DeliveryMethodType } from '@/app/components/workflow/nodes/human-input/types'
|
||||
|
||||
type HumanInputFormListProps = {
|
||||
humanInputFormDataList: HumanInputFormData[]
|
||||
onHumanInputFormSubmit?: (formToken: string, formData: any) => Promise<void>
|
||||
}
|
||||
|
||||
const HumanInputFormList = ({
|
||||
humanInputFormDataList,
|
||||
onHumanInputFormSubmit,
|
||||
}: HumanInputFormListProps) => {
|
||||
const store = useStoreApi()
|
||||
|
||||
const getHumanInputNodeData = useCallback((nodeID: string) => {
|
||||
const {
|
||||
getNodes,
|
||||
} = store.getState()
|
||||
const nodes = getNodes().filter(node => node.type === CUSTOM_NODE)
|
||||
const node = nodes.find(n => n.id === nodeID)
|
||||
return node
|
||||
}, [store])
|
||||
|
||||
const deliveryMethodsConfig = useMemo((): Record<string, { showEmailTip: boolean, isEmailDebugMode: boolean, showDebugModeTip: boolean }> => {
|
||||
if (!humanInputFormDataList.length)
|
||||
return {}
|
||||
return humanInputFormDataList.reduce((acc, formData) => {
|
||||
const deliveryMethodsConfig = getHumanInputNodeData(formData.node_id)?.data.delivery_methods || []
|
||||
if (!deliveryMethodsConfig.length) {
|
||||
acc[formData.node_id] = {
|
||||
showEmailTip: false,
|
||||
isEmailDebugMode: false,
|
||||
showDebugModeTip: false,
|
||||
}
|
||||
return acc
|
||||
}
|
||||
const isWebappEnabled = deliveryMethodsConfig.some((method: DeliveryMethod) => method.type === DeliveryMethodType.WebApp && method.enabled)
|
||||
const isEmailEnabled = deliveryMethodsConfig.some((method: DeliveryMethod) => method.type === DeliveryMethodType.Email && method.enabled)
|
||||
const isEmailDebugMode = deliveryMethodsConfig.some((method: DeliveryMethod) => method.type === DeliveryMethodType.Email && method.config?.debug_mode)
|
||||
acc[formData.node_id] = {
|
||||
showEmailTip: isEmailEnabled,
|
||||
isEmailDebugMode,
|
||||
showDebugModeTip: !isWebappEnabled,
|
||||
}
|
||||
return acc
|
||||
}, {} as Record<string, { showEmailTip: boolean, isEmailDebugMode: boolean, showDebugModeTip: boolean }>)
|
||||
}, [getHumanInputNodeData, humanInputFormDataList])
|
||||
|
||||
const filteredHumanInputFormDataList = useMemo(() => {
|
||||
return humanInputFormDataList.filter(formData => formData.display_in_ui)
|
||||
}, [humanInputFormDataList])
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-3">
|
||||
{
|
||||
filteredHumanInputFormDataList.map(formData => (
|
||||
<ContentWrapper
|
||||
key={formData.node_id}
|
||||
nodeTitle={formData.node_title}
|
||||
className="bg-components-panel-bg"
|
||||
>
|
||||
<UnsubmittedHumanInputContent
|
||||
key={formData.node_id}
|
||||
formData={formData}
|
||||
showEmailTip={!!deliveryMethodsConfig[formData.node_id]?.showEmailTip}
|
||||
isEmailDebugMode={!!deliveryMethodsConfig[formData.node_id]?.isEmailDebugMode}
|
||||
showDebugModeTip={!!deliveryMethodsConfig[formData.node_id]?.showDebugModeTip}
|
||||
onSubmit={onHumanInputFormSubmit}
|
||||
/>
|
||||
</ContentWrapper>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default HumanInputFormList
|
||||
@ -44,15 +44,18 @@ const InputsPanel = ({ onRun }: Props) => {
|
||||
const startVariables = startNode?.data.variables
|
||||
const { checkInputsForm } = useCheckInputsForms()
|
||||
|
||||
const initialInputs = { ...inputs }
|
||||
if (startVariables) {
|
||||
startVariables.forEach((variable) => {
|
||||
if (variable.default)
|
||||
initialInputs[variable.variable] = variable.default
|
||||
if (inputs[variable.variable] !== undefined)
|
||||
initialInputs[variable.variable] = inputs[variable.variable]
|
||||
})
|
||||
}
|
||||
const initialInputs = useMemo(() => {
|
||||
const result = { ...inputs }
|
||||
if (startVariables) {
|
||||
startVariables.forEach((variable) => {
|
||||
if (variable.default)
|
||||
result[variable.variable] = variable.default
|
||||
if (inputs[variable.variable] !== undefined)
|
||||
result[variable.variable] = inputs[variable.variable]
|
||||
})
|
||||
}
|
||||
return result
|
||||
}, [inputs, startVariables])
|
||||
|
||||
const variables = useMemo(() => {
|
||||
const data = startVariables || []
|
||||
@ -97,10 +100,7 @@ const InputsPanel = ({ onRun }: Props) => {
|
||||
}, [files, handleRun, initialInputs, onRun, variables, checkInputsForm])
|
||||
|
||||
const canRun = useMemo(() => {
|
||||
if (files?.some(item => (item.transfer_method as any) === TransferMethod.local_file && !item.upload_file_id))
|
||||
return false
|
||||
|
||||
return true
|
||||
return !(files?.some(item => (item.transfer_method as any) === TransferMethod.local_file && !item.upload_file_id))
|
||||
}, [files])
|
||||
|
||||
return (
|
||||
|
||||
@ -15,6 +15,7 @@ import Button from '@/app/components/base/button'
|
||||
import { RefreshCcw01 } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { submitHumanInputForm } from '@/service/workflow'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import Toast from '../../base/toast'
|
||||
import {
|
||||
@ -28,6 +29,8 @@ import {
|
||||
WorkflowRunningStatus,
|
||||
} from '../types'
|
||||
import { formatWorkflowRunIdentifier } from '../utils'
|
||||
import HumanInputFilledFormList from './human-input-filled-form-list'
|
||||
import HumanInputFormList from './human-input-form-list'
|
||||
import InputsPanel from './inputs-panel'
|
||||
|
||||
const WorkflowPreview = () => {
|
||||
@ -39,6 +42,8 @@ const WorkflowPreview = () => {
|
||||
const workflowCanvasWidth = useStore(s => s.workflowCanvasWidth)
|
||||
const panelWidth = useStore(s => s.previewPanelWidth)
|
||||
const setPreviewPanelWidth = useStore(s => s.setPreviewPanelWidth)
|
||||
const humanInputFormDataList = useStore(s => s.workflowRunningData?.humanInputFormDataList)
|
||||
const humanInputFilledFormDataList = useStore(s => s.workflowRunningData?.humanInputFilledFormDataList)
|
||||
const [userSelectedTab, setUserSelectedTab] = useState<string | null>(null)
|
||||
|
||||
const effectiveTab = (() => {
|
||||
@ -51,6 +56,9 @@ const WorkflowPreview = () => {
|
||||
&& !workflowRunningData.resultText
|
||||
&& !workflowRunningData.result.files?.length
|
||||
|
||||
if (status === WorkflowRunningStatus.Paused && humanInputFormDataList?.length)
|
||||
return 'RESULT'
|
||||
|
||||
if (isFinishedWithoutOutput && userSelectedTab === null)
|
||||
return 'DETAIL'
|
||||
|
||||
@ -103,6 +111,13 @@ const WorkflowPreview = () => {
|
||||
}
|
||||
}, [resize, stopResizing])
|
||||
|
||||
const handleSubmitHumanInputForm = useCallback(async (formToken: string, formData: {
|
||||
inputs: Record<string, string>
|
||||
action: string
|
||||
}) => {
|
||||
await submitHumanInputForm(formToken, formData)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative flex h-full flex-col rounded-l-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-xl"
|
||||
@ -198,9 +213,21 @@ const WorkflowPreview = () => {
|
||||
: null}
|
||||
{effectiveTab === 'RESULT'
|
||||
? (
|
||||
<>
|
||||
<div className="p-2">
|
||||
{humanInputFormDataList && humanInputFormDataList.length > 0 && (
|
||||
<HumanInputFormList
|
||||
humanInputFormDataList={humanInputFormDataList}
|
||||
onHumanInputFormSubmit={handleSubmitHumanInputForm}
|
||||
/>
|
||||
)}
|
||||
{humanInputFilledFormDataList && humanInputFilledFormDataList.length > 0 && (
|
||||
<HumanInputFilledFormList
|
||||
humanInputFilledFormDataList={humanInputFilledFormDataList}
|
||||
/>
|
||||
)}
|
||||
<ResultText
|
||||
isRunning={workflowRunningData?.result?.status === WorkflowRunningStatus.Running || !workflowRunningData?.result}
|
||||
isPaused={workflowRunningData?.result?.status === WorkflowRunningStatus.Paused}
|
||||
outputs={workflowRunningData?.resultText}
|
||||
llmGenerationItems={workflowRunningData?.resultLLMGenerationItems}
|
||||
allFiles={workflowRunningData?.result?.files}
|
||||
@ -223,7 +250,7 @@ const WorkflowPreview = () => {
|
||||
<div>{t('operation.copy', { ns: 'common' })}</div>
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
: null}
|
||||
{effectiveTab === 'DETAIL'
|
||||
|
||||
Reference in New Issue
Block a user