feat: Human Input node (Frontend Part) (#31631)

Co-authored-by: JzoNg <jzongcode@gmail.com>
Co-authored-by: Joel <iamjoel007@gmail.com>
Co-authored-by: yessenia <yessenia.contact@gmail.com>
Co-authored-by: QuantumGhost <obelisk.reg+git@gmail.com>
This commit is contained in:
Wu Tianwei
2026-01-30 10:16:46 +08:00
committed by GitHub
parent 5bf0251554
commit fedd097f63
198 changed files with 10955 additions and 1683 deletions

View File

@ -1,133 +0,0 @@
import type { FeedbackType } from '@/app/components/base/chat/chat/type'
import { cleanup, render, screen } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import Result from './content'
// Only mock react-i18next for translations
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}))
// Mock copy-to-clipboard for the Header component
vi.mock('copy-to-clipboard', () => ({
default: vi.fn(() => true),
}))
// Mock the format function from service/base
vi.mock('@/service/base', () => ({
format: (content: string) => content.replace(/\n/g, '<br>'),
}))
afterEach(() => {
cleanup()
})
describe('Result (content)', () => {
const mockOnFeedback = vi.fn()
const defaultProps = {
content: 'Test content here',
showFeedback: true,
feedback: { rating: null } as FeedbackType,
onFeedback: mockOnFeedback,
}
beforeEach(() => {
vi.clearAllMocks()
})
describe('rendering', () => {
it('should render the Header component', () => {
render(<Result {...defaultProps} />)
// Header renders the result title
expect(screen.getByText('generation.resultTitle')).toBeInTheDocument()
})
it('should render content', () => {
render(<Result {...defaultProps} />)
expect(screen.getByText('Test content here')).toBeInTheDocument()
})
it('should render formatted content with line breaks', () => {
render(
<Result
{...defaultProps}
content={'Line 1\nLine 2'}
/>,
)
// The format function converts \n to <br>
const contentDiv = document.querySelector('[class*="overflow-scroll"]')
expect(contentDiv?.innerHTML).toContain('Line 1<br>Line 2')
})
it('should have max height style', () => {
render(<Result {...defaultProps} />)
const contentDiv = document.querySelector('[class*="overflow-scroll"]')
expect(contentDiv).toHaveStyle({ maxHeight: '70vh' })
})
it('should render with empty content', () => {
render(
<Result
{...defaultProps}
content=""
/>,
)
expect(screen.getByText('generation.resultTitle')).toBeInTheDocument()
})
it('should render with HTML content safely', () => {
render(
<Result
{...defaultProps}
content="<script>alert('xss')</script>"
/>,
)
// Content is rendered via dangerouslySetInnerHTML
const contentDiv = document.querySelector('[class*="overflow-scroll"]')
expect(contentDiv).toBeInTheDocument()
})
})
describe('feedback props', () => {
it('should pass showFeedback to Header', () => {
render(
<Result
{...defaultProps}
showFeedback={false}
/>,
)
// Feedback buttons should not be visible
const feedbackArea = document.querySelector('[class*="space-x-1 rounded-lg border"]')
expect(feedbackArea).not.toBeInTheDocument()
})
it('should pass feedback to Header', () => {
render(
<Result
{...defaultProps}
feedback={{ rating: 'like' }}
/>,
)
// Like button should be highlighted
const likeButton = document.querySelector('[class*="primary"]')
expect(likeButton).toBeInTheDocument()
})
})
describe('memoization', () => {
it('should be wrapped with React.memo', () => {
expect((Result as unknown as { $$typeof: symbol }).$$typeof).toBe(Symbol.for('react.memo'))
})
})
})

View File

@ -1,35 +0,0 @@
import type { FC } from 'react'
import type { FeedbackType } from '@/app/components/base/chat/chat/type'
import * as React from 'react'
import { format } from '@/service/base'
import Header from './header'
export type IResultProps = {
content: string
showFeedback: boolean
feedback: FeedbackType
onFeedback: (feedback: FeedbackType) => void
}
const Result: FC<IResultProps> = ({
content,
showFeedback,
feedback,
onFeedback,
}) => {
return (
<div className="h-max basis-3/4">
<Header result={content} showFeedback={showFeedback} feedback={feedback} onFeedback={onFeedback} />
<div
className="mt-4 flex w-full overflow-scroll text-sm font-normal leading-5 text-gray-900"
style={{
maxHeight: '70vh',
}}
dangerouslySetInnerHTML={{
__html: format(content),
}}
>
</div>
</div>
)
}
export default React.memo(Result)

View File

@ -1,176 +0,0 @@
import type { FeedbackType } from '@/app/components/base/chat/chat/type'
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import Header from './header'
// Only mock react-i18next for translations
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}))
// Mock copy-to-clipboard
const mockCopy = vi.fn((_text: string) => true)
vi.mock('copy-to-clipboard', () => ({
default: (text: string) => mockCopy(text),
}))
afterEach(() => {
cleanup()
})
describe('Header', () => {
const mockOnFeedback = vi.fn()
const defaultProps = {
result: 'Test result content',
showFeedback: true,
feedback: { rating: null } as FeedbackType,
onFeedback: mockOnFeedback,
}
beforeEach(() => {
vi.clearAllMocks()
})
describe('rendering', () => {
it('should render the result title', () => {
render(<Header {...defaultProps} />)
expect(screen.getByText('generation.resultTitle')).toBeInTheDocument()
})
it('should render the copy button', () => {
render(<Header {...defaultProps} />)
expect(screen.getByText('generation.copy')).toBeInTheDocument()
})
})
describe('copy functionality', () => {
it('should copy result when copy button is clicked', () => {
render(<Header {...defaultProps} />)
const copyButton = screen.getByText('generation.copy').closest('button')
fireEvent.click(copyButton!)
expect(mockCopy).toHaveBeenCalledWith('Test result content')
})
})
describe('feedback buttons when showFeedback is true', () => {
it('should show feedback buttons when no rating is given', () => {
render(<Header {...defaultProps} />)
// Should show both thumbs up and down buttons
const buttons = document.querySelectorAll('[class*="cursor-pointer"]')
expect(buttons.length).toBeGreaterThan(0)
})
it('should show like button highlighted when rating is like', () => {
render(
<Header
{...defaultProps}
feedback={{ rating: 'like' }}
/>,
)
// Should show the undo button for like
const likeButton = document.querySelector('[class*="primary"]')
expect(likeButton).toBeInTheDocument()
})
it('should show dislike button highlighted when rating is dislike', () => {
render(
<Header
{...defaultProps}
feedback={{ rating: 'dislike' }}
/>,
)
// Should show the undo button for dislike
const dislikeButton = document.querySelector('[class*="red"]')
expect(dislikeButton).toBeInTheDocument()
})
it('should call onFeedback with like when thumbs up is clicked', () => {
render(<Header {...defaultProps} />)
// Find the thumbs up button (first one in the feedback area)
const thumbButtons = document.querySelectorAll('[class*="cursor-pointer"]')
const thumbsUp = Array.from(thumbButtons).find(btn =>
btn.className.includes('rounded-md') && !btn.className.includes('primary'),
)
if (thumbsUp) {
fireEvent.click(thumbsUp)
expect(mockOnFeedback).toHaveBeenCalledWith({ rating: 'like' })
}
})
it('should call onFeedback with dislike when thumbs down is clicked', () => {
render(<Header {...defaultProps} />)
// Find the thumbs down button
const thumbButtons = document.querySelectorAll('[class*="cursor-pointer"]')
const thumbsDown = Array.from(thumbButtons).pop()
if (thumbsDown) {
fireEvent.click(thumbsDown)
expect(mockOnFeedback).toHaveBeenCalledWith({ rating: 'dislike' })
}
})
it('should call onFeedback with null when undo like is clicked', () => {
render(
<Header
{...defaultProps}
feedback={{ rating: 'like' }}
/>,
)
// When liked, clicking the like button again should undo it (has bg-primary-100 class)
const likeButton = document.querySelector('[class*="bg-primary-100"]')
expect(likeButton).toBeInTheDocument()
fireEvent.click(likeButton!)
expect(mockOnFeedback).toHaveBeenCalledWith({ rating: null })
})
it('should call onFeedback with null when undo dislike is clicked', () => {
render(
<Header
{...defaultProps}
feedback={{ rating: 'dislike' }}
/>,
)
// When disliked, clicking the dislike button again should undo it (has bg-red-100 class)
const dislikeButton = document.querySelector('[class*="bg-red-100"]')
expect(dislikeButton).toBeInTheDocument()
fireEvent.click(dislikeButton!)
expect(mockOnFeedback).toHaveBeenCalledWith({ rating: null })
})
})
describe('feedback buttons when showFeedback is false', () => {
it('should not show feedback buttons', () => {
render(
<Header
{...defaultProps}
showFeedback={false}
/>,
)
// Should not show feedback area buttons (only copy button)
const feedbackArea = document.querySelector('[class*="space-x-1 rounded-lg border"]')
expect(feedbackArea).not.toBeInTheDocument()
})
})
describe('memoization', () => {
it('should be wrapped with React.memo', () => {
expect((Header as unknown as { $$typeof: symbol }).$$typeof).toBe(Symbol.for('react.memo'))
})
})
})

View File

@ -1,117 +0,0 @@
'use client'
import type { FC } from 'react'
import type { FeedbackType } from '@/app/components/base/chat/chat/type'
import { ClipboardDocumentIcon, HandThumbDownIcon, HandThumbUpIcon } from '@heroicons/react/24/outline'
import copy from 'copy-to-clipboard'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import Toast from '@/app/components/base/toast'
import Tooltip from '@/app/components/base/tooltip'
type IResultHeaderProps = {
result: string
showFeedback: boolean
feedback: FeedbackType
onFeedback: (feedback: FeedbackType) => void
}
const Header: FC<IResultHeaderProps> = ({
feedback,
showFeedback,
onFeedback,
result,
}) => {
const { t } = useTranslation()
return (
<div className="flex w-full items-center justify-between ">
<div className="text-2xl font-normal leading-4 text-gray-800">{t('generation.resultTitle', { ns: 'share' })}</div>
<div className="flex items-center space-x-2">
<Button
className="h-7 p-[2px] pr-2"
onClick={() => {
copy(result)
Toast.notify({ type: 'success', message: 'copied' })
}}
>
<>
<ClipboardDocumentIcon className="mr-1 h-3 w-4 text-gray-500" />
<span className="text-xs leading-3 text-gray-500">{t('generation.copy', { ns: 'share' })}</span>
</>
</Button>
{showFeedback && feedback.rating && feedback.rating === 'like' && (
<Tooltip
popupContent="Undo Great Rating"
>
<div
onClick={() => {
onFeedback({
rating: null,
})
}}
className="flex h-7 w-7 cursor-pointer items-center justify-center rounded-md border border-primary-200 bg-primary-100 !text-primary-600 hover:border-primary-300 hover:bg-primary-200"
>
<HandThumbUpIcon width={16} height={16} />
</div>
</Tooltip>
)}
{showFeedback && feedback.rating && feedback.rating === 'dislike' && (
<Tooltip
popupContent="Undo Undesirable Response"
>
<div
onClick={() => {
onFeedback({
rating: null,
})
}}
className="flex h-7 w-7 cursor-pointer items-center justify-center rounded-md border border-red-200 bg-red-100 !text-red-600 hover:border-red-300 hover:bg-red-200"
>
<HandThumbDownIcon width={16} height={16} />
</div>
</Tooltip>
)}
{showFeedback && !feedback.rating && (
<div className="flex space-x-1 rounded-lg border border-gray-200 p-[1px]">
<Tooltip
popupContent="Great Rating"
needsDelay={false}
>
<div
onClick={() => {
onFeedback({
rating: 'like',
})
}}
className="flex h-6 w-6 cursor-pointer items-center justify-center rounded-md hover:bg-gray-100"
>
<HandThumbUpIcon width={16} height={16} />
</div>
</Tooltip>
<Tooltip
popupContent="Undesirable Response"
needsDelay={false}
>
<div
onClick={() => {
onFeedback({
rating: 'dislike',
})
}}
className="flex h-6 w-6 cursor-pointer items-center justify-center rounded-md hover:bg-gray-100"
>
<HandThumbDownIcon width={16} height={16} />
</div>
</Tooltip>
</div>
)}
</div>
</div>
)
}
export default React.memo(Header)

View File

@ -5,7 +5,9 @@ import type { WorkflowProcess } from '@/app/components/base/chat/types'
import type { FileEntity } from '@/app/components/base/file-uploader/types'
import type { PromptConfig } from '@/models/debug'
import type { SiteInfo } from '@/models/share'
import type { AppSourceType } from '@/service/share'
import type {
IOtherOptions,
} from '@/service/base'
import type { VisionFile, VisionSettings } from '@/types/app'
import { RiLoader2Line } from '@remixicon/react'
import { useBoolean } from 'ahooks'
@ -25,7 +27,17 @@ import Toast from '@/app/components/base/toast'
import NoData from '@/app/components/share/text-generation/no-data'
import { NodeRunningStatus, WorkflowRunningStatus } from '@/app/components/workflow/types'
import { TEXT_GENERATION_TIMEOUT_MS } from '@/config'
import { sendCompletionMessage, sendWorkflowMessage, stopChatMessageResponding, stopWorkflowMessage, updateFeedback } from '@/service/share'
import {
sseGet,
} from '@/service/base'
import {
AppSourceType,
sendCompletionMessage,
sendWorkflowMessage,
stopChatMessageResponding,
stopWorkflowMessage,
updateFeedback,
} from '@/service/share'
import { TransferMethod } from '@/types/app'
import { sleep } from '@/utils'
import { formatBooleanInputs } from '@/utils/model-config'
@ -93,10 +105,10 @@ const Result: FC<IResultProps> = ({
const getCompletionRes = () => completionResRef.current
const [workflowProcessData, doSetWorkflowProcessData] = useState<WorkflowProcess>()
const workflowProcessDataRef = useRef<WorkflowProcess | undefined>(undefined)
const setWorkflowProcessData = (data: WorkflowProcess) => {
const setWorkflowProcessData = useCallback((data: WorkflowProcess | undefined) => {
workflowProcessDataRef.current = data
doSetWorkflowProcessData(data)
}
}, [])
const getWorkflowProcessData = () => workflowProcessDataRef.current
const [currentTaskId, setCurrentTaskId] = useState<string | null>(null)
const [isStopping, setIsStopping] = useState(false)
@ -157,7 +169,7 @@ const Result: FC<IResultProps> = ({
finally {
setIsStopping(false)
}
}, [appId, currentTaskId, appSourceType, appId, isStopping, isWorkflow, notify])
}, [appId, currentTaskId, appSourceType, isStopping, isWorkflow, notify])
useEffect(() => {
if (!onRunControlChange)
@ -257,6 +269,7 @@ const Result: FC<IResultProps> = ({
rating: null,
})
setCompletionRes('')
setWorkflowProcessData(undefined)
resetRunState()
let res: string[] = []
@ -281,10 +294,17 @@ const Result: FC<IResultProps> = ({
})()
if (isWorkflow) {
sendWorkflowMessage(
data,
{
onWorkflowStarted: ({ workflow_run_id, task_id }) => {
const otherOptions: IOtherOptions = {
isPublicAPI: appSourceType === AppSourceType.webApp,
onWorkflowStarted: ({ workflow_run_id, task_id }) => {
const workflowProcessData = getWorkflowProcessData()
if (workflowProcessData && workflowProcessData.tracing.length > 0) {
setWorkflowProcessData(produce(workflowProcessData, (draft) => {
draft.expand = true
draft.status = WorkflowRunningStatus.Running
}))
}
else {
tempMessageId = workflow_run_id
setCurrentTaskId(task_id || null)
setIsStopping(false)
@ -294,178 +314,258 @@ const Result: FC<IResultProps> = ({
expand: false,
resultText: '',
})
},
onIterationStart: ({ data }) => {
setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
draft.expand = true
draft.tracing!.push({
...data,
status: NodeRunningStatus.Running,
expand: true,
})
}))
},
onIterationNext: () => {
setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
draft.expand = true
const iterations = draft.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([])
}))
},
onIterationFinish: ({ data }) => {
setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
draft.expand = true
const iterationsIndex = draft.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))!
draft.tracing[iterationsIndex] = {
...data,
expand: !!data.error,
}
}))
},
onLoopStart: ({ data }) => {
setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
draft.expand = true
draft.tracing!.push({
...data,
status: NodeRunningStatus.Running,
expand: true,
})
}))
},
onLoopNext: () => {
setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
draft.expand = true
const loops = draft.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))!
loops?.details!.push([])
}))
},
onLoopFinish: ({ data }) => {
setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
draft.expand = true
const loopsIndex = draft.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))!
draft.tracing[loopsIndex] = {
...data,
expand: !!data.error,
}
}))
},
onNodeStarted: ({ data }) => {
if (data.iteration_id)
return
}
},
onIterationStart: ({ data }) => {
setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
draft.expand = true
draft.tracing!.push({
...data,
status: NodeRunningStatus.Running,
expand: true,
})
}))
},
onIterationNext: () => {
setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
draft.expand = true
const iterations = draft.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([])
}))
},
onIterationFinish: ({ data }) => {
setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
draft.expand = true
const iterationsIndex = draft.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))!
draft.tracing[iterationsIndex] = {
...data,
expand: !!data.error,
}
}))
},
onLoopStart: ({ data }) => {
setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
draft.expand = true
draft.tracing!.push({
...data,
status: NodeRunningStatus.Running,
expand: true,
})
}))
},
onLoopNext: () => {
setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
draft.expand = true
const loops = draft.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))!
loops?.details!.push([])
}))
},
onLoopFinish: ({ data }) => {
setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
draft.expand = true
const loopsIndex = draft.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))!
draft.tracing[loopsIndex] = {
...data,
expand: !!data.error,
}
}))
},
onNodeStarted: ({ data }) => {
if (data.iteration_id)
return
if (data.loop_id)
return
setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
draft.expand = true
draft.tracing!.push({
...data,
status: NodeRunningStatus.Running,
expand: true,
})
}))
},
onNodeFinished: ({ data }) => {
if (data.iteration_id)
return
if (data.loop_id)
return
setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
const currentIndex = draft.tracing!.findIndex(trace => trace.node_id === data.node_id
&& (trace.execution_metadata?.parallel_id === data.execution_metadata?.parallel_id || trace.parallel_id === data.execution_metadata?.parallel_id))
if (currentIndex > -1 && draft.tracing) {
draft.tracing[currentIndex] = {
...(draft.tracing[currentIndex].extras
? { extras: draft.tracing[currentIndex].extras }
: {}),
if (data.loop_id)
return
const workflowProcessData = getWorkflowProcessData()
setWorkflowProcessData(produce(workflowProcessData!, (draft) => {
if (draft.tracing.length > 0) {
const currentIndex = draft.tracing.findIndex(item => item.node_id === data.node_id)
if (currentIndex > -1) {
draft.expand = true
draft.tracing![currentIndex] = {
...data,
expand: !!data.error,
status: NodeRunningStatus.Running,
expand: true,
}
}
}))
},
onWorkflowFinished: ({ data }) => {
if (isTimeout) {
notify({ type: 'warning', message: t('warningMessage.timeoutExceeded', { ns: 'appDebug' }) })
return
}
const workflowStatus = data.status as WorkflowRunningStatus | undefined
const markNodesStopped = (traces?: WorkflowProcess['tracing']) => {
if (!traces)
return
const markTrace = (trace: WorkflowProcess['tracing'][number]) => {
if ([NodeRunningStatus.Running, NodeRunningStatus.Waiting].includes(trace.status as NodeRunningStatus))
trace.status = NodeRunningStatus.Stopped
trace.details?.forEach(detailGroup => detailGroup.forEach(markTrace))
trace.retryDetail?.forEach(markTrace)
trace.parallelDetail?.children?.forEach(markTrace)
else {
draft.expand = true
draft.tracing.push({
...data,
status: NodeRunningStatus.Running,
expand: true,
})
}
traces.forEach(markTrace)
}
if (workflowStatus === WorkflowRunningStatus.Stopped) {
setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
draft.status = WorkflowRunningStatus.Stopped
markNodesStopped(draft.tracing)
}))
setRespondingFalse()
resetRunState()
onCompleted(getCompletionRes(), taskId, false)
isEnd = true
return
}
if (data.error) {
notify({ type: 'error', message: data.error })
setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
draft.status = WorkflowRunningStatus.Failed
markNodesStopped(draft.tracing)
}))
setRespondingFalse()
resetRunState()
onCompleted(getCompletionRes(), taskId, false)
isEnd = true
return
}
setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
draft.status = WorkflowRunningStatus.Succeeded
draft.files = getFilesInLogs(data.outputs || []) as any[]
}))
if (!data.outputs) {
setCompletionRes('')
}
else {
setCompletionRes(data.outputs)
const isStringOutput = Object.keys(data.outputs).length === 1 && typeof data.outputs[Object.keys(data.outputs)[0]] === 'string'
if (isStringOutput) {
setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
draft.resultText = data.outputs[Object.keys(data.outputs)[0]]
}))
draft.expand = true
draft.tracing!.push({
...data,
status: NodeRunningStatus.Running,
expand: true,
})
}
}))
},
onNodeFinished: ({ data }) => {
if (data.iteration_id)
return
if (data.loop_id)
return
setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
const currentIndex = draft.tracing!.findIndex(trace => trace.node_id === data.node_id
&& (trace.execution_metadata?.parallel_id === data.execution_metadata?.parallel_id || trace.parallel_id === data.execution_metadata?.parallel_id))
if (currentIndex > -1 && draft.tracing) {
draft.tracing[currentIndex] = {
...(draft.tracing[currentIndex].extras
? { extras: draft.tracing[currentIndex].extras }
: {}),
...data,
expand: !!data.error,
}
}
}))
},
onWorkflowFinished: ({ data }) => {
if (isTimeout) {
notify({ type: 'warning', message: t('warningMessage.timeoutExceeded', { ns: 'appDebug' }) })
return
}
const workflowStatus = data.status as WorkflowRunningStatus | undefined
const markNodesStopped = (traces?: WorkflowProcess['tracing']) => {
if (!traces)
return
const markTrace = (trace: WorkflowProcess['tracing'][number]) => {
if ([NodeRunningStatus.Running, NodeRunningStatus.Waiting].includes(trace.status as NodeRunningStatus))
trace.status = NodeRunningStatus.Stopped
trace.details?.forEach(detailGroup => detailGroup.forEach(markTrace))
trace.retryDetail?.forEach(markTrace)
trace.parallelDetail?.children?.forEach(markTrace)
}
traces.forEach(markTrace)
}
if (workflowStatus === WorkflowRunningStatus.Stopped) {
setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
draft.status = WorkflowRunningStatus.Stopped
markNodesStopped(draft.tracing)
}))
setRespondingFalse()
resetRunState()
setMessageId(tempMessageId)
onCompleted(getCompletionRes(), taskId, true)
onCompleted(getCompletionRes(), taskId, false)
isEnd = true
},
onTextChunk: (params) => {
const { data: { text } } = params
return
}
if (data.error) {
notify({ type: 'error', message: data.error })
setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
draft.resultText += text
draft.status = WorkflowRunningStatus.Failed
markNodesStopped(draft.tracing)
}))
},
onTextReplace: (params) => {
const { data: { text } } = params
setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
draft.resultText = text
}))
},
setRespondingFalse()
resetRunState()
onCompleted(getCompletionRes(), taskId, false)
isEnd = true
return
}
setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
draft.status = WorkflowRunningStatus.Succeeded
draft.files = getFilesInLogs(data.outputs || []) as any[]
}))
if (!data.outputs) {
setCompletionRes('')
}
else {
setCompletionRes(data.outputs)
const isStringOutput = Object.keys(data.outputs).length === 1 && typeof data.outputs[Object.keys(data.outputs)[0]] === 'string'
if (isStringOutput) {
setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
draft.resultText = data.outputs[Object.keys(data.outputs)[0]]
}))
}
}
setRespondingFalse()
resetRunState()
setMessageId(tempMessageId)
onCompleted(getCompletionRes(), taskId, true)
isEnd = true
},
onTextChunk: (params) => {
const { data: { text } } = params
setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
draft.resultText += text
}))
},
onTextReplace: (params) => {
const { data: { text } } = params
setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
draft.resultText = text
}))
},
onHumanInputRequired: ({ data: humanInputRequiredData }) => {
const workflowProcessData = getWorkflowProcessData()
setWorkflowProcessData(produce(workflowProcessData!, (draft) => {
if (!draft.humanInputFormDataList) {
draft.humanInputFormDataList = [humanInputRequiredData]
}
else {
const currentFormIndex = draft.humanInputFormDataList.findIndex(item => item.node_id === humanInputRequiredData.node_id)
if (currentFormIndex > -1) {
draft.humanInputFormDataList[currentFormIndex] = humanInputRequiredData
}
else {
draft.humanInputFormDataList.push(humanInputRequiredData)
}
}
const currentIndex = draft.tracing!.findIndex(item => item.node_id === humanInputRequiredData.node_id)
if (currentIndex > -1) {
draft.tracing![currentIndex].status = NodeRunningStatus.Paused
}
}))
},
onHumanInputFormFilled: ({ data: humanInputFilledFormData }) => {
setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
if (draft.humanInputFormDataList?.length) {
const currentFormIndex = draft.humanInputFormDataList.findIndex(item => item.node_id === humanInputFilledFormData.node_id)
draft.humanInputFormDataList.splice(currentFormIndex, 1)
}
if (!draft.humanInputFilledFormDataList) {
draft.humanInputFilledFormDataList = [humanInputFilledFormData]
}
else {
draft.humanInputFilledFormDataList.push(humanInputFilledFormData)
}
}))
},
onHumanInputFormTimeout: ({ data: humanInputFormTimeoutData }) => {
setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
if (draft.humanInputFormDataList?.length) {
const currentFormIndex = draft.humanInputFormDataList.findIndex(item => item.node_id === humanInputFormTimeoutData.node_id)
draft.humanInputFormDataList[currentFormIndex].expiration_time = humanInputFormTimeoutData.expiration_time
}
}))
},
onWorkflowPaused: ({ data: workflowPausedData }) => {
const url = `/workflow/${workflowPausedData.workflow_run_id}/events`
sseGet(
url,
{},
otherOptions,
)
setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
draft.expand = false
draft.status = WorkflowRunningStatus.Paused
}))
},
}
sendWorkflowMessage(
data,
otherOptions,
appSourceType,
appId,
).catch((error) => {
@ -562,7 +662,8 @@ const Result: FC<IResultProps> = ({
isMobile={isMobile}
appSourceType={appSourceType}
installedAppId={appId}
isLoading={isCallBatchAPI ? (!completionRes && isResponding) : false}
// isLoading={isCallBatchAPI ? (!completionRes && isResponding) : false}
isLoading={false}
taskId={isCallBatchAPI ? ((taskId as number) < 10 ? `0${taskId}` : `${taskId}`) : undefined}
controlClearMoreLikeThis={controlClearMoreLikeThis}
isShowTextToSpeech={isShowTextToSpeech}