mirror of
https://github.com/langgenius/dify.git
synced 2026-05-06 02:18:08 +08:00
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:
@ -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'))
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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)
|
||||
@ -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'))
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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)
|
||||
@ -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}
|
||||
|
||||
Reference in New Issue
Block a user