| setShowMessageLogModal(true) | setShowMessageLogModal(true) | ||||
| }, [item, setCurrentLogItem, setCurrentLogModalActiveTab, setShowMessageLogModal]) | }, [item, setCurrentLogItem, setCurrentLogModalActiveTab, setShowMessageLogModal]) | ||||
| const showRetryDetail = useCallback(() => { | |||||
| setCurrentLogItem(item) | |||||
| setCurrentLogModalActiveTab('TRACING') | |||||
| setShowMessageLogModal(true) | |||||
| }, [item, setCurrentLogItem, setCurrentLogModalActiveTab, setShowMessageLogModal]) | |||||
| return ( | return ( | ||||
| <div | <div | ||||
| className={cn( | className={cn( | ||||
| <TracingPanel | <TracingPanel | ||||
| list={data.tracing} | list={data.tracing} | ||||
| onShowIterationDetail={showIterationDetail} | onShowIterationDetail={showIterationDetail} | ||||
| onShowRetryDetail={showRetryDetail} | |||||
| hideNodeInfo={hideInfo} | hideNodeInfo={hideInfo} | ||||
| hideNodeProcessDetail={hideProcessDetail} | hideNodeProcessDetail={hideProcessDetail} | ||||
| /> | /> |
| destructive?: boolean | destructive?: boolean | ||||
| wrapperClassName?: string | wrapperClassName?: string | ||||
| styleCss?: CSSProperties | styleCss?: CSSProperties | ||||
| unit?: string | |||||
| } & React.InputHTMLAttributes<HTMLInputElement> & VariantProps<typeof inputVariants> | } & React.InputHTMLAttributes<HTMLInputElement> & VariantProps<typeof inputVariants> | ||||
| const Input = ({ | const Input = ({ | ||||
| value, | value, | ||||
| placeholder, | placeholder, | ||||
| onChange, | onChange, | ||||
| unit, | |||||
| ...props | ...props | ||||
| }: InputProps) => { | }: InputProps) => { | ||||
| const { t } = useTranslation() | const { t } = useTranslation() | ||||
| {destructive && ( | {destructive && ( | ||||
| <RiErrorWarningLine className='absolute right-2 top-1/2 -translate-y-1/2 w-4 h-4 text-text-destructive-secondary' /> | <RiErrorWarningLine className='absolute right-2 top-1/2 -translate-y-1/2 w-4 h-4 text-text-destructive-secondary' /> | ||||
| )} | )} | ||||
| { | |||||
| unit && ( | |||||
| <div className='absolute right-2 top-1/2 -translate-y-1/2 system-sm-regular text-text-tertiary'> | |||||
| {unit} | |||||
| </div> | |||||
| ) | |||||
| } | |||||
| </div> | </div> | ||||
| ) | ) | ||||
| } | } |
| export const CUSTOM_NODE = 'custom' | export const CUSTOM_NODE = 'custom' | ||||
| export const CUSTOM_EDGE = 'custom' | export const CUSTOM_EDGE = 'custom' | ||||
| export const DSL_EXPORT_CHECK = 'DSL_EXPORT_CHECK' | export const DSL_EXPORT_CHECK = 'DSL_EXPORT_CHECK' | ||||
| export const DEFAULT_RETRY_MAX = 3 | |||||
| export const DEFAULT_RETRY_INTERVAL = 100 |
| getFilesInLogs, | getFilesInLogs, | ||||
| } from '@/app/components/base/file-uploader/utils' | } from '@/app/components/base/file-uploader/utils' | ||||
| import { ErrorHandleTypeEnum } from '@/app/components/workflow/nodes/_base/components/error-handle/types' | import { ErrorHandleTypeEnum } from '@/app/components/workflow/nodes/_base/components/error-handle/types' | ||||
| import type { NodeTracing } from '@/types/workflow' | |||||
| export const useWorkflowRun = () => { | export const useWorkflowRun = () => { | ||||
| const store = useStoreApi() | const store = useStoreApi() | ||||
| onIterationStart, | onIterationStart, | ||||
| onIterationNext, | onIterationNext, | ||||
| onIterationFinish, | onIterationFinish, | ||||
| onNodeRetry, | |||||
| onError, | onError, | ||||
| ...restCallback | ...restCallback | ||||
| } = callback || {} | } = callback || {} | ||||
| }) | }) | ||||
| if (currentIndex > -1 && draft.tracing) { | if (currentIndex > -1 && draft.tracing) { | ||||
| draft.tracing[currentIndex] = { | draft.tracing[currentIndex] = { | ||||
| ...data, | |||||
| ...(draft.tracing[currentIndex].extras | ...(draft.tracing[currentIndex].extras | ||||
| ? { extras: draft.tracing[currentIndex].extras } | ? { extras: draft.tracing[currentIndex].extras } | ||||
| : {}), | : {}), | ||||
| ...data, | |||||
| ...(draft.tracing[currentIndex].retryDetail | |||||
| ? { retryDetail: draft.tracing[currentIndex].retryDetail } | |||||
| : {}), | |||||
| } as any | } as any | ||||
| } | } | ||||
| })) | })) | ||||
| if (onIterationFinish) | if (onIterationFinish) | ||||
| onIterationFinish(params) | onIterationFinish(params) | ||||
| }, | }, | ||||
| onNodeRetry: (params) => { | |||||
| const { data } = params | |||||
| const { | |||||
| workflowRunningData, | |||||
| setWorkflowRunningData, | |||||
| } = workflowStore.getState() | |||||
| const { | |||||
| getNodes, | |||||
| setNodes, | |||||
| } = store.getState() | |||||
| const nodes = getNodes() | |||||
| setWorkflowRunningData(produce(workflowRunningData!, (draft) => { | |||||
| const tracing = draft.tracing! | |||||
| const currentRetryNodeIndex = tracing.findIndex(trace => trace.node_id === data.node_id) | |||||
| if (currentRetryNodeIndex > -1) { | |||||
| const currentRetryNode = tracing[currentRetryNodeIndex] | |||||
| if (currentRetryNode.retryDetail) | |||||
| draft.tracing![currentRetryNodeIndex].retryDetail!.push(data as NodeTracing) | |||||
| else | |||||
| draft.tracing![currentRetryNodeIndex].retryDetail = [data as NodeTracing] | |||||
| } | |||||
| })) | |||||
| const newNodes = produce(nodes, (draft) => { | |||||
| const currentNode = draft.find(node => node.id === data.node_id)! | |||||
| currentNode.data._retryIndex = data.retry_index | |||||
| }) | |||||
| setNodes(newNodes) | |||||
| if (onNodeRetry) | |||||
| onNodeRetry(params) | |||||
| }, | |||||
| onParallelBranchStarted: (params) => { | onParallelBranchStarted: (params) => { | ||||
| // console.log(params, 'parallel start') | // console.log(params, 'parallel start') | ||||
| }, | }, |
| import Toast from '@/app/components/base/toast' | import Toast from '@/app/components/base/toast' | ||||
| import { TransferMethod } from '@/types/app' | import { TransferMethod } from '@/types/app' | ||||
| import { getProcessedFiles } from '@/app/components/base/file-uploader/utils' | import { getProcessedFiles } from '@/app/components/base/file-uploader/utils' | ||||
| import type { NodeTracing } from '@/types/workflow' | |||||
| import RetryResultPanel from '@/app/components/workflow/run/retry-result-panel' | |||||
| import type { BlockEnum } from '@/app/components/workflow/types' | |||||
| import type { Emoji } from '@/app/components/tools/types' | |||||
| const i18nPrefix = 'workflow.singleRun' | const i18nPrefix = 'workflow.singleRun' | ||||
| type BeforeRunFormProps = { | type BeforeRunFormProps = { | ||||
| nodeName: string | nodeName: string | ||||
| nodeType?: BlockEnum | |||||
| toolIcon?: string | Emoji | |||||
| onHide: () => void | onHide: () => void | ||||
| onRun: (submitData: Record<string, any>) => void | onRun: (submitData: Record<string, any>) => void | ||||
| onStop: () => void | onStop: () => void | ||||
| runningStatus: NodeRunningStatus | runningStatus: NodeRunningStatus | ||||
| result?: JSX.Element | result?: JSX.Element | ||||
| forms: FormProps[] | forms: FormProps[] | ||||
| retryDetails?: NodeTracing[] | |||||
| onRetryDetailBack?: any | |||||
| } | } | ||||
| function formatValue(value: string | any, type: InputVarType) { | function formatValue(value: string | any, type: InputVarType) { | ||||
| } | } | ||||
| const BeforeRunForm: FC<BeforeRunFormProps> = ({ | const BeforeRunForm: FC<BeforeRunFormProps> = ({ | ||||
| nodeName, | nodeName, | ||||
| nodeType, | |||||
| toolIcon, | |||||
| onHide, | onHide, | ||||
| onRun, | onRun, | ||||
| onStop, | onStop, | ||||
| runningStatus, | runningStatus, | ||||
| result, | result, | ||||
| forms, | forms, | ||||
| retryDetails, | |||||
| onRetryDetailBack = () => { }, | |||||
| }) => { | }) => { | ||||
| const { t } = useTranslation() | const { t } = useTranslation() | ||||
| <div className='text-base font-semibold text-gray-900 truncate'> | <div className='text-base font-semibold text-gray-900 truncate'> | ||||
| {t(`${i18nPrefix}.testRun`)} {nodeName} | {t(`${i18nPrefix}.testRun`)} {nodeName} | ||||
| </div> | </div> | ||||
| <div className='ml-2 shrink-0 p-1 cursor-pointer' onClick={onHide}> | |||||
| <div className='ml-2 shrink-0 p-1 cursor-pointer' onClick={() => { | |||||
| onHide() | |||||
| }}> | |||||
| <RiCloseLine className='w-4 h-4 text-gray-500 ' /> | <RiCloseLine className='w-4 h-4 text-gray-500 ' /> | ||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| <div className='h-0 grow overflow-y-auto pb-4'> | |||||
| <div className='mt-3 px-4 space-y-4'> | |||||
| {forms.map((form, index) => ( | |||||
| <div key={index}> | |||||
| <Form | |||||
| key={index} | |||||
| className={cn(index < forms.length - 1 && 'mb-4')} | |||||
| {...form} | |||||
| /> | |||||
| {index < forms.length - 1 && <Split />} | |||||
| { | |||||
| retryDetails?.length && ( | |||||
| <div className='h-0 grow overflow-y-auto pb-4'> | |||||
| <RetryResultPanel | |||||
| list={retryDetails.map((item, index) => ({ | |||||
| ...item, | |||||
| title: `${t('workflow.nodes.common.retry.retry')} ${index + 1}`, | |||||
| node_type: nodeType!, | |||||
| extras: { | |||||
| icon: toolIcon!, | |||||
| }, | |||||
| }))} | |||||
| onBack={onRetryDetailBack} | |||||
| /> | |||||
| </div> | |||||
| ) | |||||
| } | |||||
| { | |||||
| !retryDetails?.length && ( | |||||
| <div className='h-0 grow overflow-y-auto pb-4'> | |||||
| <div className='mt-3 px-4 space-y-4'> | |||||
| {forms.map((form, index) => ( | |||||
| <div key={index}> | |||||
| <Form | |||||
| key={index} | |||||
| className={cn(index < forms.length - 1 && 'mb-4')} | |||||
| {...form} | |||||
| /> | |||||
| {index < forms.length - 1 && <Split />} | |||||
| </div> | |||||
| ))} | |||||
| </div> | </div> | ||||
| ))} | |||||
| </div> | |||||
| <div className='mt-4 flex justify-between space-x-2 px-4' > | |||||
| {isRunning && ( | |||||
| <div | |||||
| className='p-2 rounded-lg border border-gray-200 bg-white shadow-xs cursor-pointer' | |||||
| onClick={onStop} | |||||
| > | |||||
| <StopCircle className='w-4 h-4 text-gray-500' /> | |||||
| <div className='mt-4 flex justify-between space-x-2 px-4' > | |||||
| {isRunning && ( | |||||
| <div | |||||
| className='p-2 rounded-lg border border-gray-200 bg-white shadow-xs cursor-pointer' | |||||
| onClick={onStop} | |||||
| > | |||||
| <StopCircle className='w-4 h-4 text-gray-500' /> | |||||
| </div> | |||||
| )} | |||||
| <Button disabled={!isFileLoaded || isRunning} variant='primary' className='w-0 grow space-x-2' onClick={handleRun}> | |||||
| {isRunning && <RiLoader2Line className='animate-spin w-4 h-4 text-white' />} | |||||
| <div>{t(`${i18nPrefix}.${isRunning ? 'running' : 'startRun'}`)}</div> | |||||
| </Button> | |||||
| </div> | </div> | ||||
| )} | |||||
| <Button disabled={!isFileLoaded || isRunning} variant='primary' className='w-0 grow space-x-2' onClick={handleRun}> | |||||
| {isRunning && <RiLoader2Line className='animate-spin w-4 h-4 text-white' />} | |||||
| <div>{t(`${i18nPrefix}.${isRunning ? 'running' : 'startRun'}`)}</div> | |||||
| </Button> | |||||
| </div> | |||||
| {isRunning && ( | |||||
| <ResultPanel status='running' showSteps={false} /> | |||||
| )} | |||||
| {isFinished && ( | |||||
| <> | |||||
| {result} | |||||
| </> | |||||
| )} | |||||
| </div> | |||||
| {isRunning && ( | |||||
| <ResultPanel status='running' showSteps={false} /> | |||||
| )} | |||||
| {isFinished && ( | |||||
| <> | |||||
| {result} | |||||
| </> | |||||
| )} | |||||
| </div> | |||||
| ) | |||||
| } | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| ) | ) |
| CommonNodeType, | CommonNodeType, | ||||
| Node, | Node, | ||||
| } from '@/app/components/workflow/types' | } from '@/app/components/workflow/types' | ||||
| import Split from '@/app/components/workflow/nodes/_base/components/split' | |||||
| import Tooltip from '@/app/components/base/tooltip' | import Tooltip from '@/app/components/base/tooltip' | ||||
| type ErrorHandleProps = Pick<Node, 'id' | 'data'> | type ErrorHandleProps = Pick<Node, 'id' | 'data'> | ||||
| return ( | return ( | ||||
| <> | <> | ||||
| <Split /> | |||||
| <div className='py-4'> | <div className='py-4'> | ||||
| <Collapse | <Collapse | ||||
| disabled={!error_strategy} | disabled={!error_strategy} |
| import { | |||||
| useCallback, | |||||
| useState, | |||||
| } from 'react' | |||||
| import type { WorkflowRetryConfig } from './types' | |||||
| import { | |||||
| useNodeDataUpdate, | |||||
| } from '@/app/components/workflow/hooks' | |||||
| import type { NodeTracing } from '@/types/workflow' | |||||
| export const useRetryConfig = ( | |||||
| id: string, | |||||
| ) => { | |||||
| const { handleNodeDataUpdateWithSyncDraft } = useNodeDataUpdate() | |||||
| const handleRetryConfigChange = useCallback((value?: WorkflowRetryConfig) => { | |||||
| handleNodeDataUpdateWithSyncDraft({ | |||||
| id, | |||||
| data: { | |||||
| retry_config: value, | |||||
| }, | |||||
| }) | |||||
| }, [id, handleNodeDataUpdateWithSyncDraft]) | |||||
| return { | |||||
| handleRetryConfigChange, | |||||
| } | |||||
| } | |||||
| export const useRetryDetailShowInSingleRun = () => { | |||||
| const [retryDetails, setRetryDetails] = useState<NodeTracing[] | undefined>() | |||||
| const handleRetryDetailsChange = useCallback((details: NodeTracing[] | undefined) => { | |||||
| setRetryDetails(details) | |||||
| }, []) | |||||
| return { | |||||
| retryDetails, | |||||
| handleRetryDetailsChange, | |||||
| } | |||||
| } |
| import { useMemo } from 'react' | |||||
| import { useTranslation } from 'react-i18next' | |||||
| import { | |||||
| RiAlertFill, | |||||
| RiCheckboxCircleFill, | |||||
| RiLoader2Line, | |||||
| } from '@remixicon/react' | |||||
| import type { Node } from '@/app/components/workflow/types' | |||||
| import { NodeRunningStatus } from '@/app/components/workflow/types' | |||||
| import cn from '@/utils/classnames' | |||||
| type RetryOnNodeProps = Pick<Node, 'id' | 'data'> | |||||
| const RetryOnNode = ({ | |||||
| data, | |||||
| }: RetryOnNodeProps) => { | |||||
| const { t } = useTranslation() | |||||
| const { retry_config } = data | |||||
| const showSelectedBorder = data.selected || data._isBundled || data._isEntering | |||||
| const { | |||||
| isRunning, | |||||
| isSuccessful, | |||||
| isException, | |||||
| isFailed, | |||||
| } = useMemo(() => { | |||||
| return { | |||||
| isRunning: data._runningStatus === NodeRunningStatus.Running && !showSelectedBorder, | |||||
| isSuccessful: data._runningStatus === NodeRunningStatus.Succeeded && !showSelectedBorder, | |||||
| isFailed: data._runningStatus === NodeRunningStatus.Failed && !showSelectedBorder, | |||||
| isException: data._runningStatus === NodeRunningStatus.Exception && !showSelectedBorder, | |||||
| } | |||||
| }, [data._runningStatus, showSelectedBorder]) | |||||
| const showDefault = !isRunning && !isSuccessful && !isException && !isFailed | |||||
| if (!retry_config) | |||||
| return null | |||||
| return ( | |||||
| <div className='px-3'> | |||||
| <div className={cn( | |||||
| 'flex items-center justify-between px-[5px] py-1 bg-workflow-block-parma-bg border-[0.5px] border-transparent rounded-md system-xs-medium-uppercase text-text-tertiary', | |||||
| isRunning && 'bg-state-accent-hover border-state-accent-active text-text-accent', | |||||
| isSuccessful && 'bg-state-success-hover border-state-success-active text-text-success', | |||||
| (isException || isFailed) && 'bg-state-warning-hover border-state-warning-active text-text-warning', | |||||
| )}> | |||||
| <div className='flex items-center'> | |||||
| { | |||||
| showDefault && ( | |||||
| t('workflow.nodes.common.retry.retryTimes', { times: retry_config.max_retries }) | |||||
| ) | |||||
| } | |||||
| { | |||||
| isRunning && ( | |||||
| <> | |||||
| <RiLoader2Line className='animate-spin mr-1 w-3.5 h-3.5' /> | |||||
| {t('workflow.nodes.common.retry.retrying')} | |||||
| </> | |||||
| ) | |||||
| } | |||||
| { | |||||
| isSuccessful && ( | |||||
| <> | |||||
| <RiCheckboxCircleFill className='mr-1 w-3.5 h-3.5' /> | |||||
| {t('workflow.nodes.common.retry.retrySuccessful')} | |||||
| </> | |||||
| ) | |||||
| } | |||||
| { | |||||
| (isFailed || isException) && ( | |||||
| <> | |||||
| <RiAlertFill className='mr-1 w-3.5 h-3.5' /> | |||||
| {t('workflow.nodes.common.retry.retryFailed')} | |||||
| </> | |||||
| ) | |||||
| } | |||||
| </div> | |||||
| { | |||||
| !showDefault && ( | |||||
| <div> | |||||
| {data._retryIndex}/{data.retry_config?.max_retries} | |||||
| </div> | |||||
| ) | |||||
| } | |||||
| </div> | |||||
| </div> | |||||
| ) | |||||
| } | |||||
| export default RetryOnNode |
| import { useTranslation } from 'react-i18next' | |||||
| import { useRetryConfig } from './hooks' | |||||
| import s from './style.module.css' | |||||
| import Switch from '@/app/components/base/switch' | |||||
| import Slider from '@/app/components/base/slider' | |||||
| import Input from '@/app/components/base/input' | |||||
| import type { | |||||
| Node, | |||||
| } from '@/app/components/workflow/types' | |||||
| import Split from '@/app/components/workflow/nodes/_base/components/split' | |||||
| type RetryOnPanelProps = Pick<Node, 'id' | 'data'> | |||||
| const RetryOnPanel = ({ | |||||
| id, | |||||
| data, | |||||
| }: RetryOnPanelProps) => { | |||||
| const { t } = useTranslation() | |||||
| const { handleRetryConfigChange } = useRetryConfig(id) | |||||
| const { retry_config } = data | |||||
| const handleRetryEnabledChange = (value: boolean) => { | |||||
| handleRetryConfigChange({ | |||||
| retry_enabled: value, | |||||
| max_retries: retry_config?.max_retries || 3, | |||||
| retry_interval: retry_config?.retry_interval || 1000, | |||||
| }) | |||||
| } | |||||
| const handleMaxRetriesChange = (value: number) => { | |||||
| if (value > 10) | |||||
| value = 10 | |||||
| else if (value < 1) | |||||
| value = 1 | |||||
| handleRetryConfigChange({ | |||||
| retry_enabled: true, | |||||
| max_retries: value, | |||||
| retry_interval: retry_config?.retry_interval || 1000, | |||||
| }) | |||||
| } | |||||
| const handleRetryIntervalChange = (value: number) => { | |||||
| if (value > 5000) | |||||
| value = 5000 | |||||
| else if (value < 100) | |||||
| value = 100 | |||||
| handleRetryConfigChange({ | |||||
| retry_enabled: true, | |||||
| max_retries: retry_config?.max_retries || 3, | |||||
| retry_interval: value, | |||||
| }) | |||||
| } | |||||
| return ( | |||||
| <> | |||||
| <div className='pt-2'> | |||||
| <div className='flex items-center justify-between px-4 py-2 h-10'> | |||||
| <div className='flex items-center'> | |||||
| <div className='mr-0.5 system-sm-semibold-uppercase text-text-secondary'>{t('workflow.nodes.common.retry.retryOnFailure')}</div> | |||||
| </div> | |||||
| <Switch | |||||
| defaultValue={retry_config?.retry_enabled} | |||||
| onChange={v => handleRetryEnabledChange(v)} | |||||
| /> | |||||
| </div> | |||||
| { | |||||
| retry_config?.retry_enabled && ( | |||||
| <div className='px-4 pb-2'> | |||||
| <div className='flex items-center mb-1 w-full'> | |||||
| <div className='grow mr-2 system-xs-medium-uppercase'>{t('workflow.nodes.common.retry.maxRetries')}</div> | |||||
| <Slider | |||||
| className='mr-3 w-[108px]' | |||||
| value={retry_config?.max_retries || 3} | |||||
| onChange={handleMaxRetriesChange} | |||||
| min={1} | |||||
| max={10} | |||||
| /> | |||||
| <Input | |||||
| type='number' | |||||
| wrapperClassName='w-[80px]' | |||||
| value={retry_config?.max_retries || 3} | |||||
| onChange={e => handleMaxRetriesChange(e.target.value as any)} | |||||
| min={1} | |||||
| max={10} | |||||
| unit={t('workflow.nodes.common.retry.times') || ''} | |||||
| className={s.input} | |||||
| /> | |||||
| </div> | |||||
| <div className='flex items-center'> | |||||
| <div className='grow mr-2 system-xs-medium-uppercase'>{t('workflow.nodes.common.retry.retryInterval')}</div> | |||||
| <Slider | |||||
| className='mr-3 w-[108px]' | |||||
| value={retry_config?.retry_interval || 1000} | |||||
| onChange={handleRetryIntervalChange} | |||||
| min={100} | |||||
| max={5000} | |||||
| /> | |||||
| <Input | |||||
| type='number' | |||||
| wrapperClassName='w-[80px]' | |||||
| value={retry_config?.retry_interval || 1000} | |||||
| onChange={e => handleRetryIntervalChange(e.target.value as any)} | |||||
| min={100} | |||||
| max={5000} | |||||
| unit={t('workflow.nodes.common.retry.ms') || ''} | |||||
| className={s.input} | |||||
| /> | |||||
| </div> | |||||
| </div> | |||||
| ) | |||||
| } | |||||
| </div> | |||||
| <Split className='mx-4 mt-2' /> | |||||
| </> | |||||
| ) | |||||
| } | |||||
| export default RetryOnPanel |
| .input::-webkit-inner-spin-button, | |||||
| .input::-webkit-outer-spin-button { | |||||
| -webkit-appearance: none; | |||||
| margin: 0; | |||||
| } |
| export type WorkflowRetryConfig = { | |||||
| max_retries: number | |||||
| retry_interval: number | |||||
| retry_enabled: boolean | |||||
| } |
| useNodesReadOnly, | useNodesReadOnly, | ||||
| useToolIcon, | useToolIcon, | ||||
| } from '../../hooks' | } from '../../hooks' | ||||
| import { hasErrorHandleNode } from '../../utils' | |||||
| import { | |||||
| hasErrorHandleNode, | |||||
| hasRetryNode, | |||||
| } from '../../utils' | |||||
| import { useNodeIterationInteractions } from '../iteration/use-interactions' | import { useNodeIterationInteractions } from '../iteration/use-interactions' | ||||
| import type { IterationNodeType } from '../iteration/types' | import type { IterationNodeType } from '../iteration/types' | ||||
| import { | import { | ||||
| import NodeResizer from './components/node-resizer' | import NodeResizer from './components/node-resizer' | ||||
| import NodeControl from './components/node-control' | import NodeControl from './components/node-control' | ||||
| import ErrorHandleOnNode from './components/error-handle/error-handle-on-node' | import ErrorHandleOnNode from './components/error-handle/error-handle-on-node' | ||||
| import RetryOnNode from './components/retry/retry-on-node' | |||||
| import AddVariablePopupWithPosition from './components/add-variable-popup-with-position' | import AddVariablePopupWithPosition from './components/add-variable-popup-with-position' | ||||
| import cn from '@/utils/classnames' | import cn from '@/utils/classnames' | ||||
| import BlockIcon from '@/app/components/workflow/block-icon' | import BlockIcon from '@/app/components/workflow/block-icon' | ||||
| </div> | </div> | ||||
| ) | ) | ||||
| } | } | ||||
| { | |||||
| hasRetryNode(data.type) && ( | |||||
| <RetryOnNode | |||||
| id={id} | |||||
| data={data} | |||||
| /> | |||||
| ) | |||||
| } | |||||
| { | { | ||||
| hasErrorHandleNode(data.type) && ( | hasErrorHandleNode(data.type) && ( | ||||
| <ErrorHandleOnNode | <ErrorHandleOnNode |
| TitleInput, | TitleInput, | ||||
| } from './components/title-description-input' | } from './components/title-description-input' | ||||
| import ErrorHandleOnPanel from './components/error-handle/error-handle-on-panel' | import ErrorHandleOnPanel from './components/error-handle/error-handle-on-panel' | ||||
| import RetryOnPanel from './components/retry/retry-on-panel' | |||||
| import { useResizePanel } from './hooks/use-resize-panel' | import { useResizePanel } from './hooks/use-resize-panel' | ||||
| import cn from '@/utils/classnames' | import cn from '@/utils/classnames' | ||||
| import BlockIcon from '@/app/components/workflow/block-icon' | import BlockIcon from '@/app/components/workflow/block-icon' | ||||
| import Split from '@/app/components/workflow/nodes/_base/components/split' | |||||
| import { | import { | ||||
| WorkflowHistoryEvent, | WorkflowHistoryEvent, | ||||
| useAvailableBlocks, | useAvailableBlocks, | ||||
| import { | import { | ||||
| canRunBySingle, | canRunBySingle, | ||||
| hasErrorHandleNode, | hasErrorHandleNode, | ||||
| hasRetryNode, | |||||
| } from '@/app/components/workflow/utils' | } from '@/app/components/workflow/utils' | ||||
| import Tooltip from '@/app/components/base/tooltip' | import Tooltip from '@/app/components/base/tooltip' | ||||
| import type { Node } from '@/app/components/workflow/types' | import type { Node } from '@/app/components/workflow/types' | ||||
| <div> | <div> | ||||
| {cloneElement(children, { id, data })} | {cloneElement(children, { id, data })} | ||||
| </div> | </div> | ||||
| <Split /> | |||||
| { | |||||
| hasRetryNode(data.type) && ( | |||||
| <RetryOnPanel | |||||
| id={id} | |||||
| data={data} | |||||
| /> | |||||
| ) | |||||
| } | |||||
| { | { | ||||
| hasErrorHandleNode(data.type) && ( | hasErrorHandleNode(data.type) && ( | ||||
| <ErrorHandleOnPanel | <ErrorHandleOnPanel |
| import type { NodeDefault } from '../../types' | import type { NodeDefault } from '../../types' | ||||
| import { AuthorizationType, BodyType, Method } from './types' | import { AuthorizationType, BodyType, Method } from './types' | ||||
| import type { BodyPayload, HttpNodeType } from './types' | import type { BodyPayload, HttpNodeType } from './types' | ||||
| import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants' | |||||
| import { | |||||
| ALL_CHAT_AVAILABLE_BLOCKS, | |||||
| ALL_COMPLETION_AVAILABLE_BLOCKS, | |||||
| } from '@/app/components/workflow/constants' | |||||
| const nodeDefault: NodeDefault<HttpNodeType> = { | const nodeDefault: NodeDefault<HttpNodeType> = { | ||||
| defaultValue: { | defaultValue: { | ||||
| max_read_timeout: 0, | max_read_timeout: 0, | ||||
| max_write_timeout: 0, | max_write_timeout: 0, | ||||
| }, | }, | ||||
| retry_config: { | |||||
| retry_enabled: true, | |||||
| max_retries: 3, | |||||
| retry_interval: 100, | |||||
| }, | |||||
| }, | }, | ||||
| getAvailablePrevNodes(isChatMode: boolean) { | getAvailablePrevNodes(isChatMode: boolean) { | ||||
| const nodes = isChatMode | const nodes = isChatMode |
| import type { FC } from 'react' | import type { FC } from 'react' | ||||
| import React from 'react' | |||||
| import { memo } from 'react' | |||||
| import { useTranslation } from 'react-i18next' | import { useTranslation } from 'react-i18next' | ||||
| import useConfig from './use-config' | import useConfig from './use-config' | ||||
| import ApiInput from './components/api-input' | import ApiInput from './components/api-input' | ||||
| import type { NodePanelProps } from '@/app/components/workflow/types' | import type { NodePanelProps } from '@/app/components/workflow/types' | ||||
| import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form' | import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form' | ||||
| import ResultPanel from '@/app/components/workflow/run/result-panel' | import ResultPanel from '@/app/components/workflow/run/result-panel' | ||||
| import { useRetryDetailShowInSingleRun } from '@/app/components/workflow/nodes/_base/components/retry/hooks' | |||||
| const i18nPrefix = 'workflow.nodes.http' | const i18nPrefix = 'workflow.nodes.http' | ||||
| hideCurlPanel, | hideCurlPanel, | ||||
| handleCurlImport, | handleCurlImport, | ||||
| } = useConfig(id, data) | } = useConfig(id, data) | ||||
| const { | |||||
| retryDetails, | |||||
| handleRetryDetailsChange, | |||||
| } = useRetryDetailShowInSingleRun() | |||||
| // To prevent prompt editor in body not update data. | // To prevent prompt editor in body not update data. | ||||
| if (!isDataReady) | if (!isDataReady) | ||||
| return null | return null | ||||
| {isShowSingleRun && ( | {isShowSingleRun && ( | ||||
| <BeforeRunForm | <BeforeRunForm | ||||
| nodeName={inputs.title} | nodeName={inputs.title} | ||||
| nodeType={inputs.type} | |||||
| onHide={hideSingleRun} | onHide={hideSingleRun} | ||||
| forms={[ | forms={[ | ||||
| { | { | ||||
| runningStatus={runningStatus} | runningStatus={runningStatus} | ||||
| onRun={handleRun} | onRun={handleRun} | ||||
| onStop={handleStop} | onStop={handleStop} | ||||
| result={<ResultPanel {...runResult} showSteps={false} />} | |||||
| retryDetails={retryDetails} | |||||
| onRetryDetailBack={handleRetryDetailsChange} | |||||
| result={<ResultPanel {...runResult} showSteps={false} onShowRetryDetail={handleRetryDetailsChange} />} | |||||
| /> | /> | ||||
| )} | )} | ||||
| {(isShowCurlPanel && !readOnly) && ( | {(isShowCurlPanel && !readOnly) && ( | ||||
| ) | ) | ||||
| } | } | ||||
| export default React.memo(Panel) | |||||
| export default memo(Panel) |
| import ResultPanel from '@/app/components/workflow/run/result-panel' | import ResultPanel from '@/app/components/workflow/run/result-panel' | ||||
| import Tooltip from '@/app/components/base/tooltip' | import Tooltip from '@/app/components/base/tooltip' | ||||
| import Editor from '@/app/components/workflow/nodes/_base/components/prompt/editor' | import Editor from '@/app/components/workflow/nodes/_base/components/prompt/editor' | ||||
| import { useRetryDetailShowInSingleRun } from '@/app/components/workflow/nodes/_base/components/retry/hooks' | |||||
| const i18nPrefix = 'workflow.nodes.llm' | const i18nPrefix = 'workflow.nodes.llm' | ||||
| runResult, | runResult, | ||||
| filterJinjia2InputVar, | filterJinjia2InputVar, | ||||
| } = useConfig(id, data) | } = useConfig(id, data) | ||||
| const { | |||||
| retryDetails, | |||||
| handleRetryDetailsChange, | |||||
| } = useRetryDetailShowInSingleRun() | |||||
| const model = inputs.model | const model = inputs.model | ||||
| {isShowSingleRun && ( | {isShowSingleRun && ( | ||||
| <BeforeRunForm | <BeforeRunForm | ||||
| nodeName={inputs.title} | nodeName={inputs.title} | ||||
| nodeType={inputs.type} | |||||
| onHide={hideSingleRun} | onHide={hideSingleRun} | ||||
| forms={singleRunForms} | forms={singleRunForms} | ||||
| runningStatus={runningStatus} | runningStatus={runningStatus} | ||||
| onRun={handleRun} | onRun={handleRun} | ||||
| onStop={handleStop} | onStop={handleStop} | ||||
| result={<ResultPanel {...runResult} showSteps={false} />} | |||||
| retryDetails={retryDetails} | |||||
| onRetryDetailBack={handleRetryDetailsChange} | |||||
| result={<ResultPanel {...runResult} showSteps={false} onShowRetryDetail={handleRetryDetailsChange} />} | |||||
| /> | /> | ||||
| )} | )} | ||||
| </div> | </div> |
| import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form' | import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form' | ||||
| import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars' | import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars' | ||||
| import ResultPanel from '@/app/components/workflow/run/result-panel' | import ResultPanel from '@/app/components/workflow/run/result-panel' | ||||
| import { useRetryDetailShowInSingleRun } from '@/app/components/workflow/nodes/_base/components/retry/hooks' | |||||
| import { useToolIcon } from '@/app/components/workflow/hooks' | |||||
| const i18nPrefix = 'workflow.nodes.tool' | const i18nPrefix = 'workflow.nodes.tool' | ||||
| handleStop, | handleStop, | ||||
| runResult, | runResult, | ||||
| } = useConfig(id, data) | } = useConfig(id, data) | ||||
| const toolIcon = useToolIcon(data) | |||||
| const { | |||||
| retryDetails, | |||||
| handleRetryDetailsChange, | |||||
| } = useRetryDetailShowInSingleRun() | |||||
| if (isLoading) { | if (isLoading) { | ||||
| return <div className='flex h-[200px] items-center justify-center'> | return <div className='flex h-[200px] items-center justify-center'> | ||||
| {isShowSingleRun && ( | {isShowSingleRun && ( | ||||
| <BeforeRunForm | <BeforeRunForm | ||||
| nodeName={inputs.title} | nodeName={inputs.title} | ||||
| nodeType={inputs.type} | |||||
| toolIcon={toolIcon} | |||||
| onHide={hideSingleRun} | onHide={hideSingleRun} | ||||
| forms={singleRunForms} | forms={singleRunForms} | ||||
| runningStatus={runningStatus} | runningStatus={runningStatus} | ||||
| onRun={handleRun} | onRun={handleRun} | ||||
| onStop={handleStop} | onStop={handleStop} | ||||
| result={<ResultPanel {...runResult} showSteps={false} />} | |||||
| retryDetails={retryDetails} | |||||
| onRetryDetailBack={handleRetryDetailsChange} | |||||
| result={<ResultPanel {...runResult} showSteps={false} onShowRetryDetail={handleRetryDetailsChange} />} | |||||
| /> | /> | ||||
| )} | )} | ||||
| </div> | </div> |
| getProcessedFilesFromResponse, | getProcessedFilesFromResponse, | ||||
| } from '@/app/components/base/file-uploader/utils' | } from '@/app/components/base/file-uploader/utils' | ||||
| import type { FileEntity } from '@/app/components/base/file-uploader/types' | import type { FileEntity } from '@/app/components/base/file-uploader/types' | ||||
| import type { NodeTracing } from '@/types/workflow' | |||||
| type GetAbortController = (abortController: AbortController) => void | type GetAbortController = (abortController: AbortController) => void | ||||
| type SendCallback = { | type SendCallback = { | ||||
| } | } | ||||
| })) | })) | ||||
| }, | }, | ||||
| onNodeRetry: ({ data }) => { | |||||
| if (data.iteration_id) | |||||
| return | |||||
| const currentIndex = responseItem.workflowProcess!.tracing!.findIndex((item) => { | |||||
| if (!item.execution_metadata?.parallel_id) | |||||
| return item.node_id === data.node_id | |||||
| return item.node_id === data.node_id && (item.execution_metadata?.parallel_id === data.execution_metadata?.parallel_id || item.parallel_id === data.execution_metadata?.parallel_id) | |||||
| }) | |||||
| if (responseItem.workflowProcess!.tracing[currentIndex].retryDetail) | |||||
| responseItem.workflowProcess!.tracing[currentIndex].retryDetail?.push(data as NodeTracing) | |||||
| else | |||||
| responseItem.workflowProcess!.tracing[currentIndex].retryDetail = [data as NodeTracing] | |||||
| handleUpdateChatList(produce(chatListRef.current, (draft) => { | |||||
| const currentIndex = draft.findIndex(item => item.id === responseItem.id) | |||||
| draft[currentIndex] = { | |||||
| ...draft[currentIndex], | |||||
| ...responseItem, | |||||
| } | |||||
| })) | |||||
| }, | |||||
| onNodeFinished: ({ data }) => { | onNodeFinished: ({ data }) => { | ||||
| if (data.iteration_id) | if (data.iteration_id) | ||||
| return | return | ||||
| ...(responseItem.workflowProcess!.tracing[currentIndex]?.extras | ...(responseItem.workflowProcess!.tracing[currentIndex]?.extras | ||||
| ? { extras: responseItem.workflowProcess!.tracing[currentIndex].extras } | ? { extras: responseItem.workflowProcess!.tracing[currentIndex].extras } | ||||
| : {}), | : {}), | ||||
| ...(responseItem.workflowProcess!.tracing[currentIndex]?.retryDetail | |||||
| ? { retryDetail: responseItem.workflowProcess!.tracing[currentIndex].retryDetail } | |||||
| : {}), | |||||
| ...data, | ...data, | ||||
| } as any | } as any | ||||
| handleUpdateChatList(produce(chatListRef.current, (draft) => { | handleUpdateChatList(produce(chatListRef.current, (draft) => { |
| import { SimpleBtn } from '../../app/text-generate/item' | import { SimpleBtn } from '../../app/text-generate/item' | ||||
| import Toast from '../../base/toast' | import Toast from '../../base/toast' | ||||
| import IterationResultPanel from '../run/iteration-result-panel' | import IterationResultPanel from '../run/iteration-result-panel' | ||||
| import RetryResultPanel from '../run/retry-result-panel' | |||||
| import InputsPanel from './inputs-panel' | import InputsPanel from './inputs-panel' | ||||
| import cn from '@/utils/classnames' | import cn from '@/utils/classnames' | ||||
| import Loading from '@/app/components/base/loading' | import Loading from '@/app/components/base/loading' | ||||
| }, [workflowRunningData]) | }, [workflowRunningData]) | ||||
| const [iterationRunResult, setIterationRunResult] = useState<NodeTracing[][]>([]) | const [iterationRunResult, setIterationRunResult] = useState<NodeTracing[][]>([]) | ||||
| const [retryRunResult, setRetryRunResult] = useState<NodeTracing[]>([]) | |||||
| const [iterDurationMap, setIterDurationMap] = useState<IterationDurationMap>({}) | const [iterDurationMap, setIterDurationMap] = useState<IterationDurationMap>({}) | ||||
| const [isShowIterationDetail, { | const [isShowIterationDetail, { | ||||
| setTrue: doShowIterationDetail, | setTrue: doShowIterationDetail, | ||||
| setFalse: doHideIterationDetail, | setFalse: doHideIterationDetail, | ||||
| }] = useBoolean(false) | }] = useBoolean(false) | ||||
| const [isShowRetryDetail, { | |||||
| setTrue: doShowRetryDetail, | |||||
| setFalse: doHideRetryDetail, | |||||
| }] = useBoolean(false) | |||||
| const handleShowIterationDetail = useCallback((detail: NodeTracing[][], iterationDurationMap: IterationDurationMap) => { | const handleShowIterationDetail = useCallback((detail: NodeTracing[][], iterationDurationMap: IterationDurationMap) => { | ||||
| setIterDurationMap(iterationDurationMap) | setIterDurationMap(iterationDurationMap) | ||||
| doShowIterationDetail() | doShowIterationDetail() | ||||
| }, [doShowIterationDetail]) | }, [doShowIterationDetail]) | ||||
| const handleRetryDetail = useCallback((detail: NodeTracing[]) => { | |||||
| setRetryRunResult(detail) | |||||
| doShowRetryDetail() | |||||
| }, [doShowRetryDetail]) | |||||
| if (isShowIterationDetail) { | if (isShowIterationDetail) { | ||||
| return ( | return ( | ||||
| <div className={` | <div className={` | ||||
| <Loading /> | <Loading /> | ||||
| </div> | </div> | ||||
| )} | )} | ||||
| {currentTab === 'TRACING' && ( | |||||
| {currentTab === 'TRACING' && !isShowRetryDetail && ( | |||||
| <TracingPanel | <TracingPanel | ||||
| className='bg-background-section-burn' | className='bg-background-section-burn' | ||||
| list={workflowRunningData?.tracing || []} | list={workflowRunningData?.tracing || []} | ||||
| onShowIterationDetail={handleShowIterationDetail} | onShowIterationDetail={handleShowIterationDetail} | ||||
| onShowRetryDetail={handleRetryDetail} | |||||
| /> | /> | ||||
| )} | )} | ||||
| {currentTab === 'TRACING' && !workflowRunningData?.tracing?.length && ( | {currentTab === 'TRACING' && !workflowRunningData?.tracing?.length && ( | ||||
| <Loading /> | <Loading /> | ||||
| </div> | </div> | ||||
| )} | )} | ||||
| { | |||||
| currentTab === 'TRACING' && isShowRetryDetail && ( | |||||
| <RetryResultPanel | |||||
| list={retryRunResult} | |||||
| onBack={doHideRetryDetail} | |||||
| /> | |||||
| ) | |||||
| } | |||||
| </div> | </div> | ||||
| </> | </> | ||||
| )} | )} |
| import ResultPanel from './result-panel' | import ResultPanel from './result-panel' | ||||
| import TracingPanel from './tracing-panel' | import TracingPanel from './tracing-panel' | ||||
| import IterationResultPanel from './iteration-result-panel' | import IterationResultPanel from './iteration-result-panel' | ||||
| import RetryResultPanel from './retry-result-panel' | |||||
| import cn from '@/utils/classnames' | import cn from '@/utils/classnames' | ||||
| import { ToastContext } from '@/app/components/base/toast' | import { ToastContext } from '@/app/components/base/toast' | ||||
| import Loading from '@/app/components/base/loading' | import Loading from '@/app/components/base/loading' | ||||
| const processNonIterationNode = (item: NodeTracing) => { | const processNonIterationNode = (item: NodeTracing) => { | ||||
| const { execution_metadata } = item | const { execution_metadata } = item | ||||
| if (!execution_metadata?.iteration_id) { | if (!execution_metadata?.iteration_id) { | ||||
| if (item.status === 'retry') { | |||||
| const retryNode = result.find(node => node.node_id === item.node_id) | |||||
| if (retryNode) { | |||||
| if (retryNode?.retryDetail) | |||||
| retryNode.retryDetail.push(item) | |||||
| else | |||||
| retryNode.retryDetail = [item] | |||||
| } | |||||
| return | |||||
| } | |||||
| result.push(item) | result.push(item) | ||||
| return | return | ||||
| } | } | ||||
| const [iterationRunResult, setIterationRunResult] = useState<NodeTracing[][]>([]) | const [iterationRunResult, setIterationRunResult] = useState<NodeTracing[][]>([]) | ||||
| const [iterDurationMap, setIterDurationMap] = useState<IterationDurationMap>({}) | const [iterDurationMap, setIterDurationMap] = useState<IterationDurationMap>({}) | ||||
| const [retryRunResult, setRetryRunResult] = useState<NodeTracing[]>([]) | |||||
| const [isShowIterationDetail, { | const [isShowIterationDetail, { | ||||
| setTrue: doShowIterationDetail, | setTrue: doShowIterationDetail, | ||||
| setFalse: doHideIterationDetail, | setFalse: doHideIterationDetail, | ||||
| }] = useBoolean(false) | }] = useBoolean(false) | ||||
| const [isShowRetryDetail, { | |||||
| setTrue: doShowRetryDetail, | |||||
| setFalse: doHideRetryDetail, | |||||
| }] = useBoolean(false) | |||||
| const handleShowIterationDetail = useCallback((detail: NodeTracing[][], iterDurationMap: IterationDurationMap) => { | const handleShowIterationDetail = useCallback((detail: NodeTracing[][], iterDurationMap: IterationDurationMap) => { | ||||
| setIterationRunResult(detail) | setIterationRunResult(detail) | ||||
| setIterDurationMap(iterDurationMap) | setIterDurationMap(iterDurationMap) | ||||
| }, [doShowIterationDetail, setIterationRunResult, setIterDurationMap]) | }, [doShowIterationDetail, setIterationRunResult, setIterDurationMap]) | ||||
| const handleShowRetryDetail = useCallback((detail: NodeTracing[]) => { | |||||
| setRetryRunResult(detail) | |||||
| doShowRetryDetail() | |||||
| }, [doShowRetryDetail, setRetryRunResult]) | |||||
| if (isShowIterationDetail) { | if (isShowIterationDetail) { | ||||
| return ( | return ( | ||||
| <div className='grow relative flex flex-col'> | <div className='grow relative flex flex-col'> | ||||
| exceptionCounts={runDetail.exceptions_count} | exceptionCounts={runDetail.exceptions_count} | ||||
| /> | /> | ||||
| )} | )} | ||||
| {!loading && currentTab === 'TRACING' && ( | |||||
| {!loading && currentTab === 'TRACING' && !isShowRetryDetail && ( | |||||
| <TracingPanel | <TracingPanel | ||||
| className='bg-background-section-burn' | className='bg-background-section-burn' | ||||
| list={list} | list={list} | ||||
| onShowIterationDetail={handleShowIterationDetail} | onShowIterationDetail={handleShowIterationDetail} | ||||
| onShowRetryDetail={handleShowRetryDetail} | |||||
| /> | /> | ||||
| )} | )} | ||||
| { | |||||
| !loading && currentTab === 'TRACING' && isShowRetryDetail && ( | |||||
| <RetryResultPanel | |||||
| list={retryRunResult} | |||||
| onBack={doHideRetryDetail} | |||||
| /> | |||||
| ) | |||||
| } | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| ) | ) |
| RiCheckboxCircleFill, | RiCheckboxCircleFill, | ||||
| RiErrorWarningLine, | RiErrorWarningLine, | ||||
| RiLoader2Line, | RiLoader2Line, | ||||
| RiRestartFill, | |||||
| } from '@remixicon/react' | } from '@remixicon/react' | ||||
| import BlockIcon from '../block-icon' | import BlockIcon from '../block-icon' | ||||
| import { BlockEnum } from '../types' | import { BlockEnum } from '../types' | ||||
| import { CodeLanguage } from '@/app/components/workflow/nodes/code/types' | import { CodeLanguage } from '@/app/components/workflow/nodes/code/types' | ||||
| import type { IterationDurationMap, NodeTracing } from '@/types/workflow' | import type { IterationDurationMap, NodeTracing } from '@/types/workflow' | ||||
| import ErrorHandleTip from '@/app/components/workflow/nodes/_base/components/error-handle/error-handle-tip' | import ErrorHandleTip from '@/app/components/workflow/nodes/_base/components/error-handle/error-handle-tip' | ||||
| import { hasRetryNode } from '@/app/components/workflow/utils' | |||||
| type Props = { | type Props = { | ||||
| className?: string | className?: string | ||||
| hideInfo?: boolean | hideInfo?: boolean | ||||
| hideProcessDetail?: boolean | hideProcessDetail?: boolean | ||||
| onShowIterationDetail?: (detail: NodeTracing[][], iterDurationMap: IterationDurationMap) => void | onShowIterationDetail?: (detail: NodeTracing[][], iterDurationMap: IterationDurationMap) => void | ||||
| onShowRetryDetail?: (detail: NodeTracing[]) => void | |||||
| notShowIterationNav?: boolean | notShowIterationNav?: boolean | ||||
| justShowIterationNavArrow?: boolean | justShowIterationNavArrow?: boolean | ||||
| justShowRetryNavArrow?: boolean | |||||
| } | } | ||||
| const NodePanel: FC<Props> = ({ | const NodePanel: FC<Props> = ({ | ||||
| hideInfo = false, | hideInfo = false, | ||||
| hideProcessDetail, | hideProcessDetail, | ||||
| onShowIterationDetail, | onShowIterationDetail, | ||||
| onShowRetryDetail, | |||||
| notShowIterationNav, | notShowIterationNav, | ||||
| justShowIterationNavArrow, | justShowIterationNavArrow, | ||||
| }) => { | }) => { | ||||
| }, [nodeInfo.expand, setCollapseState]) | }, [nodeInfo.expand, setCollapseState]) | ||||
| const isIterationNode = nodeInfo.node_type === BlockEnum.Iteration | const isIterationNode = nodeInfo.node_type === BlockEnum.Iteration | ||||
| const isRetryNode = hasRetryNode(nodeInfo.node_type) && nodeInfo.retryDetail | |||||
| const handleOnShowIterationDetail = (e: React.MouseEvent<HTMLButtonElement>) => { | const handleOnShowIterationDetail = (e: React.MouseEvent<HTMLButtonElement>) => { | ||||
| e.stopPropagation() | e.stopPropagation() | ||||
| e.nativeEvent.stopImmediatePropagation() | e.nativeEvent.stopImmediatePropagation() | ||||
| onShowIterationDetail?.(nodeInfo.details || [], nodeInfo?.iterDurationMap || nodeInfo.execution_metadata?.iteration_duration_map || {}) | onShowIterationDetail?.(nodeInfo.details || [], nodeInfo?.iterDurationMap || nodeInfo.execution_metadata?.iteration_duration_map || {}) | ||||
| } | } | ||||
| const handleOnShowRetryDetail = (e: React.MouseEvent<HTMLButtonElement>) => { | |||||
| e.stopPropagation() | |||||
| e.nativeEvent.stopImmediatePropagation() | |||||
| onShowRetryDetail?.(nodeInfo.retryDetail || []) | |||||
| } | |||||
| return ( | return ( | ||||
| <div className={cn('px-2 py-1', className)}> | <div className={cn('px-2 py-1', className)}> | ||||
| <div className='group transition-all bg-background-default border border-components-panel-border rounded-[10px] shadow-xs hover:shadow-md'> | <div className='group transition-all bg-background-default border border-components-panel-border rounded-[10px] shadow-xs hover:shadow-md'> | ||||
| <Split className='mt-2' /> | <Split className='mt-2' /> | ||||
| </div> | </div> | ||||
| )} | )} | ||||
| {isRetryNode && ( | |||||
| <Button | |||||
| className='flex items-center justify-between mb-1 w-full' | |||||
| variant='tertiary' | |||||
| onClick={handleOnShowRetryDetail} | |||||
| > | |||||
| <div className='flex items-center'> | |||||
| <RiRestartFill className='mr-0.5 w-4 h-4 text-components-button-tertiary-text flex-shrink-0' /> | |||||
| {t('workflow.nodes.common.retry.retries', { num: nodeInfo.retryDetail?.length })} | |||||
| </div> | |||||
| <RiArrowRightSLine className='w-4 h-4 text-components-button-tertiary-text flex-shrink-0' /> | |||||
| </Button> | |||||
| )} | |||||
| <div className={cn('mb-1', hideInfo && '!px-2 !py-0.5')}> | <div className={cn('mb-1', hideInfo && '!px-2 !py-0.5')}> | ||||
| {(nodeInfo.status === 'stopped') && ( | {(nodeInfo.status === 'stopped') && ( | ||||
| <StatusContainer status='stopped'> | <StatusContainer status='stopped'> |
| 'use client' | 'use client' | ||||
| import type { FC } from 'react' | import type { FC } from 'react' | ||||
| import { useTranslation } from 'react-i18next' | import { useTranslation } from 'react-i18next' | ||||
| import { | |||||
| RiArrowRightSLine, | |||||
| RiRestartFill, | |||||
| } from '@remixicon/react' | |||||
| import StatusPanel from './status' | import StatusPanel from './status' | ||||
| import MetaData from './meta' | import MetaData from './meta' | ||||
| import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor' | import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor' | ||||
| import { CodeLanguage } from '@/app/components/workflow/nodes/code/types' | import { CodeLanguage } from '@/app/components/workflow/nodes/code/types' | ||||
| import ErrorHandleTip from '@/app/components/workflow/nodes/_base/components/error-handle/error-handle-tip' | import ErrorHandleTip from '@/app/components/workflow/nodes/_base/components/error-handle/error-handle-tip' | ||||
| import type { NodeTracing } from '@/types/workflow' | |||||
| import Button from '@/app/components/base/button' | |||||
| type ResultPanelProps = { | type ResultPanelProps = { | ||||
| inputs?: string | inputs?: string | ||||
| showSteps?: boolean | showSteps?: boolean | ||||
| exceptionCounts?: number | exceptionCounts?: number | ||||
| execution_metadata?: any | execution_metadata?: any | ||||
| retry_events?: NodeTracing[] | |||||
| onShowRetryDetail?: (retries: NodeTracing[]) => void | |||||
| } | } | ||||
| const ResultPanel: FC<ResultPanelProps> = ({ | const ResultPanel: FC<ResultPanelProps> = ({ | ||||
| showSteps, | showSteps, | ||||
| exceptionCounts, | exceptionCounts, | ||||
| execution_metadata, | execution_metadata, | ||||
| retry_events, | |||||
| onShowRetryDetail, | |||||
| }) => { | }) => { | ||||
| const { t } = useTranslation() | const { t } = useTranslation() | ||||
| return ( | return ( | ||||
| <div className='bg-components-panel-bg py-2'> | <div className='bg-components-panel-bg py-2'> | ||||
| <div className='px-4 py-2'> | <div className='px-4 py-2'> | ||||
| exceptionCounts={exceptionCounts} | exceptionCounts={exceptionCounts} | ||||
| /> | /> | ||||
| </div> | </div> | ||||
| { | |||||
| retry_events?.length && onShowRetryDetail && ( | |||||
| <div className='px-4'> | |||||
| <Button | |||||
| className='flex items-center justify-between w-full' | |||||
| variant='tertiary' | |||||
| onClick={() => onShowRetryDetail(retry_events)} | |||||
| > | |||||
| <div className='flex items-center'> | |||||
| <RiRestartFill className='mr-0.5 w-4 h-4 text-components-button-tertiary-text flex-shrink-0' /> | |||||
| {t('workflow.nodes.common.retry.retries', { num: retry_events?.length })} | |||||
| </div> | |||||
| <RiArrowRightSLine className='w-4 h-4 text-components-button-tertiary-text flex-shrink-0' /> | |||||
| </Button> | |||||
| </div> | |||||
| ) | |||||
| } | |||||
| <div className='px-4 py-2 flex flex-col gap-2'> | <div className='px-4 py-2 flex flex-col gap-2'> | ||||
| <CodeEditor | <CodeEditor | ||||
| readOnly | readOnly |
| 'use client' | |||||
| import type { FC } from 'react' | |||||
| import { memo } from 'react' | |||||
| import { useTranslation } from 'react-i18next' | |||||
| import { | |||||
| RiArrowLeftLine, | |||||
| } from '@remixicon/react' | |||||
| import TracingPanel from './tracing-panel' | |||||
| import type { NodeTracing } from '@/types/workflow' | |||||
| type Props = { | |||||
| list: NodeTracing[] | |||||
| onBack: () => void | |||||
| } | |||||
| const RetryResultPanel: FC<Props> = ({ | |||||
| list, | |||||
| onBack, | |||||
| }) => { | |||||
| const { t } = useTranslation() | |||||
| return ( | |||||
| <div> | |||||
| <div | |||||
| className='flex items-center px-4 h-8 text-text-accent-secondary bg-components-panel-bg system-sm-medium cursor-pointer' | |||||
| onClick={(e) => { | |||||
| e.stopPropagation() | |||||
| e.nativeEvent.stopImmediatePropagation() | |||||
| onBack() | |||||
| }} | |||||
| > | |||||
| <RiArrowLeftLine className='mr-1 w-4 h-4' /> | |||||
| {t('workflow.singleRun.back')} | |||||
| </div> | |||||
| <TracingPanel | |||||
| list={list.map((item, index) => ({ | |||||
| ...item, | |||||
| title: `${t('workflow.nodes.common.retry.retry')} ${index + 1}`, | |||||
| }))} | |||||
| className='bg-background-section-burn' | |||||
| /> | |||||
| </div > | |||||
| ) | |||||
| } | |||||
| export default memo(RetryResultPanel) |
| type TracingPanelProps = { | type TracingPanelProps = { | ||||
| list: NodeTracing[] | list: NodeTracing[] | ||||
| onShowIterationDetail?: (detail: NodeTracing[][], iterDurationMap: IterationDurationMap) => void | onShowIterationDetail?: (detail: NodeTracing[][], iterDurationMap: IterationDurationMap) => void | ||||
| onShowRetryDetail?: (detail: NodeTracing[]) => void | |||||
| className?: string | className?: string | ||||
| hideNodeInfo?: boolean | hideNodeInfo?: boolean | ||||
| hideNodeProcessDetail?: boolean | hideNodeProcessDetail?: boolean | ||||
| const TracingPanel: FC<TracingPanelProps> = ({ | const TracingPanel: FC<TracingPanelProps> = ({ | ||||
| list, | list, | ||||
| onShowIterationDetail, | onShowIterationDetail, | ||||
| onShowRetryDetail, | |||||
| className, | className, | ||||
| hideNodeInfo = false, | hideNodeInfo = false, | ||||
| hideNodeProcessDetail = false, | hideNodeProcessDetail = false, | ||||
| <NodePanel | <NodePanel | ||||
| nodeInfo={node.data!} | nodeInfo={node.data!} | ||||
| onShowIterationDetail={onShowIterationDetail} | onShowIterationDetail={onShowIterationDetail} | ||||
| onShowRetryDetail={onShowRetryDetail} | |||||
| justShowIterationNavArrow={true} | justShowIterationNavArrow={true} | ||||
| justShowRetryNavArrow={true} | |||||
| hideInfo={hideNodeInfo} | hideInfo={hideNodeInfo} | ||||
| hideProcessDetail={hideNodeProcessDetail} | hideProcessDetail={hideNodeProcessDetail} | ||||
| /> | /> |
| DefaultValueForm, | DefaultValueForm, | ||||
| ErrorHandleTypeEnum, | ErrorHandleTypeEnum, | ||||
| } from '@/app/components/workflow/nodes/_base/components/error-handle/types' | } from '@/app/components/workflow/nodes/_base/components/error-handle/types' | ||||
| import type { WorkflowRetryConfig } from '@/app/components/workflow/nodes/_base/components/retry/types' | |||||
| export enum BlockEnum { | export enum BlockEnum { | ||||
| Start = 'start', | Start = 'start', | ||||
| _iterationIndex?: number | _iterationIndex?: number | ||||
| _inParallelHovering?: boolean | _inParallelHovering?: boolean | ||||
| _waitingRun?: boolean | _waitingRun?: boolean | ||||
| _retryIndex?: number | |||||
| isInIteration?: boolean | isInIteration?: boolean | ||||
| iteration_id?: string | iteration_id?: string | ||||
| selected?: boolean | selected?: boolean | ||||
| width?: number | width?: number | ||||
| height?: number | height?: number | ||||
| error_strategy?: ErrorHandleTypeEnum | error_strategy?: ErrorHandleTypeEnum | ||||
| retry_config?: WorkflowRetryConfig | |||||
| default_value?: DefaultValueForm[] | default_value?: DefaultValueForm[] | ||||
| } & T & Partial<Pick<ToolDefaultValue, 'provider_id' | 'provider_type' | 'provider_name' | 'tool_name'>> | } & T & Partial<Pick<ToolDefaultValue, 'provider_id' | 'provider_type' | 'provider_name' | 'tool_name'>> | ||||
| Succeeded = 'succeeded', | Succeeded = 'succeeded', | ||||
| Failed = 'failed', | Failed = 'failed', | ||||
| Exception = 'exception', | Exception = 'exception', | ||||
| Retry = 'retry', | |||||
| } | } | ||||
| export type OnNodeAdd = ( | export type OnNodeAdd = ( |
| } from './types' | } from './types' | ||||
| import { | import { | ||||
| CUSTOM_NODE, | CUSTOM_NODE, | ||||
| DEFAULT_RETRY_INTERVAL, | |||||
| DEFAULT_RETRY_MAX, | |||||
| ITERATION_CHILDREN_Z_INDEX, | ITERATION_CHILDREN_Z_INDEX, | ||||
| ITERATION_NODE_Z_INDEX, | ITERATION_NODE_Z_INDEX, | ||||
| NODE_WIDTH_X_OFFSET, | NODE_WIDTH_X_OFFSET, | ||||
| iterationNodeData.error_handle_mode = iterationNodeData.error_handle_mode || ErrorHandleMode.Terminated | iterationNodeData.error_handle_mode = iterationNodeData.error_handle_mode || ErrorHandleMode.Terminated | ||||
| } | } | ||||
| if (node.data.type === BlockEnum.HttpRequest && !node.data.retry_config) { | |||||
| node.data.retry_config = { | |||||
| retry_enabled: true, | |||||
| max_retries: DEFAULT_RETRY_MAX, | |||||
| retry_interval: DEFAULT_RETRY_INTERVAL, | |||||
| } | |||||
| } | |||||
| return node | return node | ||||
| }) | }) | ||||
| } | } | ||||
| return false | return false | ||||
| } | } | ||||
| export const hasRetryNode = (nodeType?: BlockEnum) => { | |||||
| return nodeType === BlockEnum.LLM || nodeType === BlockEnum.Tool || nodeType === BlockEnum.HttpRequest || nodeType === BlockEnum.Code | |||||
| } |
| tip: 'There are {{num}} nodes in the process running abnormally, please go to tracing to check the logs.', | tip: 'There are {{num}} nodes in the process running abnormally, please go to tracing to check the logs.', | ||||
| }, | }, | ||||
| }, | }, | ||||
| retry: { | |||||
| retry: 'Retry', | |||||
| retryOnFailure: 'retry on failure', | |||||
| maxRetries: 'max retries', | |||||
| retryInterval: 'retry interval', | |||||
| retryTimes: 'Retry {{times}} times on failure', | |||||
| retrying: 'Retrying...', | |||||
| retrySuccessful: 'Retry successful', | |||||
| retryFailed: 'Retry failed', | |||||
| retryFailedTimes: '{{times}} retries failed', | |||||
| times: 'times', | |||||
| ms: 'ms', | |||||
| retries: '{{num}} Retries', | |||||
| }, | |||||
| }, | }, | ||||
| start: { | start: { | ||||
| required: 'required', | required: 'required', |
| tip: '流程中有 {{num}} 个节点运行异常,请前往追踪查看日志。', | tip: '流程中有 {{num}} 个节点运行异常,请前往追踪查看日志。', | ||||
| }, | }, | ||||
| }, | }, | ||||
| retry: { | |||||
| retry: '重试', | |||||
| retryOnFailure: '失败时重试', | |||||
| maxRetries: '最大重试次数', | |||||
| retryInterval: '重试间隔', | |||||
| retryTimes: '失败时重试 {{times}} 次', | |||||
| retrying: '重试中...', | |||||
| retrySuccessful: '重试成功', | |||||
| retryFailed: '重试失败', | |||||
| retryFailedTimes: '{{times}} 次重试失败', | |||||
| times: '次', | |||||
| ms: '毫秒', | |||||
| retries: '{{num}} 重试次数', | |||||
| }, | |||||
| }, | }, | ||||
| start: { | start: { | ||||
| required: '必填', | required: '必填', |
| export type IOnNodeFinished = (nodeFinished: NodeFinishedResponse) => void | export type IOnNodeFinished = (nodeFinished: NodeFinishedResponse) => void | ||||
| export type IOnIterationStarted = (workflowStarted: IterationStartedResponse) => void | export type IOnIterationStarted = (workflowStarted: IterationStartedResponse) => void | ||||
| export type IOnIterationNext = (workflowStarted: IterationNextResponse) => void | export type IOnIterationNext = (workflowStarted: IterationNextResponse) => void | ||||
| export type IOnNodeRetry = (nodeFinished: NodeFinishedResponse) => void | |||||
| export type IOnIterationFinished = (workflowFinished: IterationFinishedResponse) => void | export type IOnIterationFinished = (workflowFinished: IterationFinishedResponse) => void | ||||
| export type IOnParallelBranchStarted = (parallelBranchStarted: ParallelBranchStartedResponse) => void | export type IOnParallelBranchStarted = (parallelBranchStarted: ParallelBranchStartedResponse) => void | ||||
| export type IOnParallelBranchFinished = (parallelBranchFinished: ParallelBranchFinishedResponse) => void | export type IOnParallelBranchFinished = (parallelBranchFinished: ParallelBranchFinishedResponse) => void | ||||
| onIterationStart?: IOnIterationStarted | onIterationStart?: IOnIterationStarted | ||||
| onIterationNext?: IOnIterationNext | onIterationNext?: IOnIterationNext | ||||
| onIterationFinish?: IOnIterationFinished | onIterationFinish?: IOnIterationFinished | ||||
| onNodeRetry?: IOnNodeRetry | |||||
| onParallelBranchStarted?: IOnParallelBranchStarted | onParallelBranchStarted?: IOnParallelBranchStarted | ||||
| onParallelBranchFinished?: IOnParallelBranchFinished | onParallelBranchFinished?: IOnParallelBranchFinished | ||||
| onTextChunk?: IOnTextChunk | onTextChunk?: IOnTextChunk | ||||
| onIterationStart?: IOnIterationStarted, | onIterationStart?: IOnIterationStarted, | ||||
| onIterationNext?: IOnIterationNext, | onIterationNext?: IOnIterationNext, | ||||
| onIterationFinish?: IOnIterationFinished, | onIterationFinish?: IOnIterationFinished, | ||||
| onNodeRetry?: IOnNodeRetry, | |||||
| onParallelBranchStarted?: IOnParallelBranchStarted, | onParallelBranchStarted?: IOnParallelBranchStarted, | ||||
| onParallelBranchFinished?: IOnParallelBranchFinished, | onParallelBranchFinished?: IOnParallelBranchFinished, | ||||
| onTextChunk?: IOnTextChunk, | onTextChunk?: IOnTextChunk, | ||||
| else if (bufferObj.event === 'iteration_completed') { | else if (bufferObj.event === 'iteration_completed') { | ||||
| onIterationFinish?.(bufferObj as IterationFinishedResponse) | onIterationFinish?.(bufferObj as IterationFinishedResponse) | ||||
| } | } | ||||
| else if (bufferObj.event === 'node_retry') { | |||||
| onNodeRetry?.(bufferObj as NodeFinishedResponse) | |||||
| } | |||||
| else if (bufferObj.event === 'parallel_branch_started') { | else if (bufferObj.event === 'parallel_branch_started') { | ||||
| onParallelBranchStarted?.(bufferObj as ParallelBranchStartedResponse) | onParallelBranchStarted?.(bufferObj as ParallelBranchStartedResponse) | ||||
| } | } | ||||
| onIterationStart, | onIterationStart, | ||||
| onIterationNext, | onIterationNext, | ||||
| onIterationFinish, | onIterationFinish, | ||||
| onNodeRetry, | |||||
| onParallelBranchStarted, | onParallelBranchStarted, | ||||
| onParallelBranchFinished, | onParallelBranchFinished, | ||||
| onTextChunk, | onTextChunk, | ||||
| return | return | ||||
| } | } | ||||
| onData?.(str, isFirstMessage, moreInfo) | onData?.(str, isFirstMessage, moreInfo) | ||||
| }, onCompleted, onThought, onMessageEnd, onMessageReplace, onFile, onWorkflowStarted, onWorkflowFinished, onNodeStarted, onNodeFinished, onIterationStart, onIterationNext, onIterationFinish, onParallelBranchStarted, onParallelBranchFinished, onTextChunk, onTTSChunk, onTTSEnd, onTextReplace) | |||||
| }, onCompleted, onThought, onMessageEnd, onMessageReplace, onFile, onWorkflowStarted, onWorkflowFinished, onNodeStarted, onNodeFinished, onIterationStart, onIterationNext, onIterationFinish, onNodeRetry, onParallelBranchStarted, onParallelBranchFinished, onTextChunk, onTTSChunk, onTTSEnd, onTextReplace) | |||||
| }).catch((e) => { | }).catch((e) => { | ||||
| if (e.toString() !== 'AbortError: The user aborted a request.' && !e.toString().errorMessage.includes('TypeError: Cannot assign to read only property')) | if (e.toString() !== 'AbortError: The user aborted a request.' && !e.toString().errorMessage.includes('TypeError: Cannot assign to read only property')) | ||||
| Toast.notify({ type: 'error', message: e }) | Toast.notify({ type: 'error', message: e }) |
| extras?: any | extras?: any | ||||
| expand?: boolean // for UI | expand?: boolean // for UI | ||||
| details?: NodeTracing[][] // iteration detail | details?: NodeTracing[][] // iteration detail | ||||
| retryDetail?: NodeTracing[] // retry detail | |||||
| parallel_id?: string | parallel_id?: string | ||||
| parallel_start_node_id?: string | parallel_start_node_id?: string | ||||
| parent_parallel_id?: string | parent_parallel_id?: string | ||||
| parent_parallel_start_node_id?: string | parent_parallel_start_node_id?: string | ||||
| retry_index?: number | |||||
| } | } | ||||
| export type FetchWorkflowDraftResponse = { | export type FetchWorkflowDraftResponse = { | ||||
| } | } | ||||
| created_at: number | created_at: number | ||||
| files?: FileResponse[] | files?: FileResponse[] | ||||
| retry_index?: number | |||||
| } | } | ||||
| } | } | ||||