| @@ -64,6 +64,12 @@ const WorkflowProcessItem = ({ | |||
| setShowMessageLogModal(true) | |||
| }, [item, setCurrentLogItem, setCurrentLogModalActiveTab, setShowMessageLogModal]) | |||
| const showRetryDetail = useCallback(() => { | |||
| setCurrentLogItem(item) | |||
| setCurrentLogModalActiveTab('TRACING') | |||
| setShowMessageLogModal(true) | |||
| }, [item, setCurrentLogItem, setCurrentLogModalActiveTab, setShowMessageLogModal]) | |||
| return ( | |||
| <div | |||
| className={cn( | |||
| @@ -105,6 +111,7 @@ const WorkflowProcessItem = ({ | |||
| <TracingPanel | |||
| list={data.tracing} | |||
| onShowIterationDetail={showIterationDetail} | |||
| onShowRetryDetail={showRetryDetail} | |||
| hideNodeInfo={hideInfo} | |||
| hideNodeProcessDetail={hideProcessDetail} | |||
| /> | |||
| @@ -28,6 +28,7 @@ export type InputProps = { | |||
| destructive?: boolean | |||
| wrapperClassName?: string | |||
| styleCss?: CSSProperties | |||
| unit?: string | |||
| } & React.InputHTMLAttributes<HTMLInputElement> & VariantProps<typeof inputVariants> | |||
| const Input = ({ | |||
| @@ -43,6 +44,7 @@ const Input = ({ | |||
| value, | |||
| placeholder, | |||
| onChange, | |||
| unit, | |||
| ...props | |||
| }: InputProps) => { | |||
| const { t } = useTranslation() | |||
| @@ -80,6 +82,13 @@ const Input = ({ | |||
| {destructive && ( | |||
| <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> | |||
| ) | |||
| } | |||
| @@ -506,3 +506,5 @@ export const WORKFLOW_DATA_UPDATE = 'WORKFLOW_DATA_UPDATE' | |||
| export const CUSTOM_NODE = 'custom' | |||
| export const CUSTOM_EDGE = 'custom' | |||
| export const DSL_EXPORT_CHECK = 'DSL_EXPORT_CHECK' | |||
| export const DEFAULT_RETRY_MAX = 3 | |||
| export const DEFAULT_RETRY_INTERVAL = 100 | |||
| @@ -28,6 +28,7 @@ import { | |||
| getFilesInLogs, | |||
| } from '@/app/components/base/file-uploader/utils' | |||
| import { ErrorHandleTypeEnum } from '@/app/components/workflow/nodes/_base/components/error-handle/types' | |||
| import type { NodeTracing } from '@/types/workflow' | |||
| export const useWorkflowRun = () => { | |||
| const store = useStoreApi() | |||
| @@ -114,6 +115,7 @@ export const useWorkflowRun = () => { | |||
| onIterationStart, | |||
| onIterationNext, | |||
| onIterationFinish, | |||
| onNodeRetry, | |||
| onError, | |||
| ...restCallback | |||
| } = callback || {} | |||
| @@ -440,10 +442,13 @@ export const useWorkflowRun = () => { | |||
| }) | |||
| if (currentIndex > -1 && draft.tracing) { | |||
| draft.tracing[currentIndex] = { | |||
| ...data, | |||
| ...(draft.tracing[currentIndex].extras | |||
| ? { extras: draft.tracing[currentIndex].extras } | |||
| : {}), | |||
| ...data, | |||
| ...(draft.tracing[currentIndex].retryDetail | |||
| ? { retryDetail: draft.tracing[currentIndex].retryDetail } | |||
| : {}), | |||
| } as any | |||
| } | |||
| })) | |||
| @@ -616,6 +621,41 @@ export const useWorkflowRun = () => { | |||
| if (onIterationFinish) | |||
| 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) => { | |||
| // console.log(params, 'parallel start') | |||
| }, | |||
| @@ -17,17 +17,25 @@ import ResultPanel from '@/app/components/workflow/run/result-panel' | |||
| import Toast from '@/app/components/base/toast' | |||
| import { TransferMethod } from '@/types/app' | |||
| 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' | |||
| type BeforeRunFormProps = { | |||
| nodeName: string | |||
| nodeType?: BlockEnum | |||
| toolIcon?: string | Emoji | |||
| onHide: () => void | |||
| onRun: (submitData: Record<string, any>) => void | |||
| onStop: () => void | |||
| runningStatus: NodeRunningStatus | |||
| result?: JSX.Element | |||
| forms: FormProps[] | |||
| retryDetails?: NodeTracing[] | |||
| onRetryDetailBack?: any | |||
| } | |||
| function formatValue(value: string | any, type: InputVarType) { | |||
| @@ -50,12 +58,16 @@ function formatValue(value: string | any, type: InputVarType) { | |||
| } | |||
| const BeforeRunForm: FC<BeforeRunFormProps> = ({ | |||
| nodeName, | |||
| nodeType, | |||
| toolIcon, | |||
| onHide, | |||
| onRun, | |||
| onStop, | |||
| runningStatus, | |||
| result, | |||
| forms, | |||
| retryDetails, | |||
| onRetryDetailBack = () => { }, | |||
| }) => { | |||
| const { t } = useTranslation() | |||
| @@ -122,48 +134,69 @@ const BeforeRunForm: FC<BeforeRunFormProps> = ({ | |||
| <div className='text-base font-semibold text-gray-900 truncate'> | |||
| {t(`${i18nPrefix}.testRun`)} {nodeName} | |||
| </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 ' /> | |||
| </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 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> | |||
| )} | |||
| <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> | |||
| ) | |||
| @@ -14,7 +14,6 @@ import type { | |||
| CommonNodeType, | |||
| Node, | |||
| } from '@/app/components/workflow/types' | |||
| import Split from '@/app/components/workflow/nodes/_base/components/split' | |||
| import Tooltip from '@/app/components/base/tooltip' | |||
| type ErrorHandleProps = Pick<Node, 'id' | 'data'> | |||
| @@ -45,7 +44,6 @@ const ErrorHandle = ({ | |||
| return ( | |||
| <> | |||
| <Split /> | |||
| <div className='py-4'> | |||
| <Collapse | |||
| disabled={!error_strategy} | |||
| @@ -0,0 +1,41 @@ | |||
| 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, | |||
| } | |||
| } | |||
| @@ -0,0 +1,88 @@ | |||
| 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 | |||
| @@ -0,0 +1,117 @@ | |||
| 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 | |||
| @@ -0,0 +1,5 @@ | |||
| .input::-webkit-inner-spin-button, | |||
| .input::-webkit-outer-spin-button { | |||
| -webkit-appearance: none; | |||
| margin: 0; | |||
| } | |||
| @@ -0,0 +1,5 @@ | |||
| export type WorkflowRetryConfig = { | |||
| max_retries: number | |||
| retry_interval: number | |||
| retry_enabled: boolean | |||
| } | |||
| @@ -25,7 +25,10 @@ import { | |||
| useNodesReadOnly, | |||
| useToolIcon, | |||
| } from '../../hooks' | |||
| import { hasErrorHandleNode } from '../../utils' | |||
| import { | |||
| hasErrorHandleNode, | |||
| hasRetryNode, | |||
| } from '../../utils' | |||
| import { useNodeIterationInteractions } from '../iteration/use-interactions' | |||
| import type { IterationNodeType } from '../iteration/types' | |||
| import { | |||
| @@ -35,6 +38,7 @@ import { | |||
| import NodeResizer from './components/node-resizer' | |||
| import NodeControl from './components/node-control' | |||
| 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 cn from '@/utils/classnames' | |||
| import BlockIcon from '@/app/components/workflow/block-icon' | |||
| @@ -237,6 +241,14 @@ const BaseNode: FC<BaseNodeProps> = ({ | |||
| </div> | |||
| ) | |||
| } | |||
| { | |||
| hasRetryNode(data.type) && ( | |||
| <RetryOnNode | |||
| id={id} | |||
| data={data} | |||
| /> | |||
| ) | |||
| } | |||
| { | |||
| hasErrorHandleNode(data.type) && ( | |||
| <ErrorHandleOnNode | |||
| @@ -21,9 +21,11 @@ import { | |||
| TitleInput, | |||
| } from './components/title-description-input' | |||
| 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 cn from '@/utils/classnames' | |||
| import BlockIcon from '@/app/components/workflow/block-icon' | |||
| import Split from '@/app/components/workflow/nodes/_base/components/split' | |||
| import { | |||
| WorkflowHistoryEvent, | |||
| useAvailableBlocks, | |||
| @@ -38,6 +40,7 @@ import { | |||
| import { | |||
| canRunBySingle, | |||
| hasErrorHandleNode, | |||
| hasRetryNode, | |||
| } from '@/app/components/workflow/utils' | |||
| import Tooltip from '@/app/components/base/tooltip' | |||
| import type { Node } from '@/app/components/workflow/types' | |||
| @@ -168,6 +171,15 @@ const BasePanel: FC<BasePanelProps> = ({ | |||
| <div> | |||
| {cloneElement(children, { id, data })} | |||
| </div> | |||
| <Split /> | |||
| { | |||
| hasRetryNode(data.type) && ( | |||
| <RetryOnPanel | |||
| id={id} | |||
| data={data} | |||
| /> | |||
| ) | |||
| } | |||
| { | |||
| hasErrorHandleNode(data.type) && ( | |||
| <ErrorHandleOnPanel | |||
| @@ -2,7 +2,10 @@ import { BlockEnum } from '../../types' | |||
| import type { NodeDefault } from '../../types' | |||
| import { AuthorizationType, BodyType, Method } 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> = { | |||
| defaultValue: { | |||
| @@ -24,6 +27,11 @@ const nodeDefault: NodeDefault<HttpNodeType> = { | |||
| max_read_timeout: 0, | |||
| max_write_timeout: 0, | |||
| }, | |||
| retry_config: { | |||
| retry_enabled: true, | |||
| max_retries: 3, | |||
| retry_interval: 100, | |||
| }, | |||
| }, | |||
| getAvailablePrevNodes(isChatMode: boolean) { | |||
| const nodes = isChatMode | |||
| @@ -1,5 +1,5 @@ | |||
| import type { FC } from 'react' | |||
| import React from 'react' | |||
| import { memo } from 'react' | |||
| import { useTranslation } from 'react-i18next' | |||
| import useConfig from './use-config' | |||
| import ApiInput from './components/api-input' | |||
| @@ -18,6 +18,7 @@ import { FileArrow01 } from '@/app/components/base/icons/src/vender/line/files' | |||
| import type { NodePanelProps } from '@/app/components/workflow/types' | |||
| import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form' | |||
| 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' | |||
| @@ -60,6 +61,10 @@ const Panel: FC<NodePanelProps<HttpNodeType>> = ({ | |||
| hideCurlPanel, | |||
| handleCurlImport, | |||
| } = useConfig(id, data) | |||
| const { | |||
| retryDetails, | |||
| handleRetryDetailsChange, | |||
| } = useRetryDetailShowInSingleRun() | |||
| // To prevent prompt editor in body not update data. | |||
| if (!isDataReady) | |||
| return null | |||
| @@ -181,6 +186,7 @@ const Panel: FC<NodePanelProps<HttpNodeType>> = ({ | |||
| {isShowSingleRun && ( | |||
| <BeforeRunForm | |||
| nodeName={inputs.title} | |||
| nodeType={inputs.type} | |||
| onHide={hideSingleRun} | |||
| forms={[ | |||
| { | |||
| @@ -192,7 +198,9 @@ const Panel: FC<NodePanelProps<HttpNodeType>> = ({ | |||
| runningStatus={runningStatus} | |||
| onRun={handleRun} | |||
| onStop={handleStop} | |||
| result={<ResultPanel {...runResult} showSteps={false} />} | |||
| retryDetails={retryDetails} | |||
| onRetryDetailBack={handleRetryDetailsChange} | |||
| result={<ResultPanel {...runResult} showSteps={false} onShowRetryDetail={handleRetryDetailsChange} />} | |||
| /> | |||
| )} | |||
| {(isShowCurlPanel && !readOnly) && ( | |||
| @@ -207,4 +215,4 @@ const Panel: FC<NodePanelProps<HttpNodeType>> = ({ | |||
| ) | |||
| } | |||
| export default React.memo(Panel) | |||
| export default memo(Panel) | |||
| @@ -19,6 +19,7 @@ import type { Props as FormProps } from '@/app/components/workflow/nodes/_base/c | |||
| import ResultPanel from '@/app/components/workflow/run/result-panel' | |||
| import Tooltip from '@/app/components/base/tooltip' | |||
| 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' | |||
| @@ -69,6 +70,10 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({ | |||
| runResult, | |||
| filterJinjia2InputVar, | |||
| } = useConfig(id, data) | |||
| const { | |||
| retryDetails, | |||
| handleRetryDetailsChange, | |||
| } = useRetryDetailShowInSingleRun() | |||
| const model = inputs.model | |||
| @@ -282,12 +287,15 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({ | |||
| {isShowSingleRun && ( | |||
| <BeforeRunForm | |||
| nodeName={inputs.title} | |||
| nodeType={inputs.type} | |||
| onHide={hideSingleRun} | |||
| forms={singleRunForms} | |||
| runningStatus={runningStatus} | |||
| onRun={handleRun} | |||
| onStop={handleStop} | |||
| result={<ResultPanel {...runResult} showSteps={false} />} | |||
| retryDetails={retryDetails} | |||
| onRetryDetailBack={handleRetryDetailsChange} | |||
| result={<ResultPanel {...runResult} showSteps={false} onShowRetryDetail={handleRetryDetailsChange} />} | |||
| /> | |||
| )} | |||
| </div> | |||
| @@ -14,6 +14,8 @@ import Loading from '@/app/components/base/loading' | |||
| 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 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' | |||
| @@ -48,6 +50,11 @@ const Panel: FC<NodePanelProps<ToolNodeType>> = ({ | |||
| handleStop, | |||
| runResult, | |||
| } = useConfig(id, data) | |||
| const toolIcon = useToolIcon(data) | |||
| const { | |||
| retryDetails, | |||
| handleRetryDetailsChange, | |||
| } = useRetryDetailShowInSingleRun() | |||
| if (isLoading) { | |||
| return <div className='flex h-[200px] items-center justify-center'> | |||
| @@ -143,12 +150,16 @@ const Panel: FC<NodePanelProps<ToolNodeType>> = ({ | |||
| {isShowSingleRun && ( | |||
| <BeforeRunForm | |||
| nodeName={inputs.title} | |||
| nodeType={inputs.type} | |||
| toolIcon={toolIcon} | |||
| onHide={hideSingleRun} | |||
| forms={singleRunForms} | |||
| runningStatus={runningStatus} | |||
| onRun={handleRun} | |||
| onStop={handleStop} | |||
| result={<ResultPanel {...runResult} showSteps={false} />} | |||
| retryDetails={retryDetails} | |||
| onRetryDetailBack={handleRetryDetailsChange} | |||
| result={<ResultPanel {...runResult} showSteps={false} onShowRetryDetail={handleRetryDetailsChange} />} | |||
| /> | |||
| )} | |||
| </div> | |||
| @@ -27,6 +27,7 @@ import { | |||
| getProcessedFilesFromResponse, | |||
| } from '@/app/components/base/file-uploader/utils' | |||
| import type { FileEntity } from '@/app/components/base/file-uploader/types' | |||
| import type { NodeTracing } from '@/types/workflow' | |||
| type GetAbortController = (abortController: AbortController) => void | |||
| type SendCallback = { | |||
| @@ -381,6 +382,28 @@ export const useChat = ( | |||
| } | |||
| })) | |||
| }, | |||
| 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 }) => { | |||
| if (data.iteration_id) | |||
| return | |||
| @@ -394,6 +417,9 @@ export const useChat = ( | |||
| ...(responseItem.workflowProcess!.tracing[currentIndex]?.extras | |||
| ? { extras: responseItem.workflowProcess!.tracing[currentIndex].extras } | |||
| : {}), | |||
| ...(responseItem.workflowProcess!.tracing[currentIndex]?.retryDetail | |||
| ? { retryDetail: responseItem.workflowProcess!.tracing[currentIndex].retryDetail } | |||
| : {}), | |||
| ...data, | |||
| } as any | |||
| handleUpdateChatList(produce(chatListRef.current, (draft) => { | |||
| @@ -25,6 +25,7 @@ import { | |||
| import { SimpleBtn } from '../../app/text-generate/item' | |||
| import Toast from '../../base/toast' | |||
| import IterationResultPanel from '../run/iteration-result-panel' | |||
| import RetryResultPanel from '../run/retry-result-panel' | |||
| import InputsPanel from './inputs-panel' | |||
| import cn from '@/utils/classnames' | |||
| import Loading from '@/app/components/base/loading' | |||
| @@ -53,11 +54,16 @@ const WorkflowPreview = () => { | |||
| }, [workflowRunningData]) | |||
| const [iterationRunResult, setIterationRunResult] = useState<NodeTracing[][]>([]) | |||
| const [retryRunResult, setRetryRunResult] = useState<NodeTracing[]>([]) | |||
| const [iterDurationMap, setIterDurationMap] = useState<IterationDurationMap>({}) | |||
| const [isShowIterationDetail, { | |||
| setTrue: doShowIterationDetail, | |||
| setFalse: doHideIterationDetail, | |||
| }] = useBoolean(false) | |||
| const [isShowRetryDetail, { | |||
| setTrue: doShowRetryDetail, | |||
| setFalse: doHideRetryDetail, | |||
| }] = useBoolean(false) | |||
| const handleShowIterationDetail = useCallback((detail: NodeTracing[][], iterationDurationMap: IterationDurationMap) => { | |||
| setIterDurationMap(iterationDurationMap) | |||
| @@ -65,6 +71,11 @@ const WorkflowPreview = () => { | |||
| doShowIterationDetail() | |||
| }, [doShowIterationDetail]) | |||
| const handleRetryDetail = useCallback((detail: NodeTracing[]) => { | |||
| setRetryRunResult(detail) | |||
| doShowRetryDetail() | |||
| }, [doShowRetryDetail]) | |||
| if (isShowIterationDetail) { | |||
| return ( | |||
| <div className={` | |||
| @@ -201,11 +212,12 @@ const WorkflowPreview = () => { | |||
| <Loading /> | |||
| </div> | |||
| )} | |||
| {currentTab === 'TRACING' && ( | |||
| {currentTab === 'TRACING' && !isShowRetryDetail && ( | |||
| <TracingPanel | |||
| className='bg-background-section-burn' | |||
| list={workflowRunningData?.tracing || []} | |||
| onShowIterationDetail={handleShowIterationDetail} | |||
| onShowRetryDetail={handleRetryDetail} | |||
| /> | |||
| )} | |||
| {currentTab === 'TRACING' && !workflowRunningData?.tracing?.length && ( | |||
| @@ -213,7 +225,14 @@ const WorkflowPreview = () => { | |||
| <Loading /> | |||
| </div> | |||
| )} | |||
| { | |||
| currentTab === 'TRACING' && isShowRetryDetail && ( | |||
| <RetryResultPanel | |||
| list={retryRunResult} | |||
| onBack={doHideRetryDetail} | |||
| /> | |||
| ) | |||
| } | |||
| </div> | |||
| </> | |||
| )} | |||
| @@ -9,6 +9,7 @@ import OutputPanel from './output-panel' | |||
| import ResultPanel from './result-panel' | |||
| import TracingPanel from './tracing-panel' | |||
| import IterationResultPanel from './iteration-result-panel' | |||
| import RetryResultPanel from './retry-result-panel' | |||
| import cn from '@/utils/classnames' | |||
| import { ToastContext } from '@/app/components/base/toast' | |||
| import Loading from '@/app/components/base/loading' | |||
| @@ -107,6 +108,18 @@ const RunPanel: FC<RunProps> = ({ hideResult, activeTab = 'RESULT', runID, getRe | |||
| const processNonIterationNode = (item: NodeTracing) => { | |||
| const { execution_metadata } = item | |||
| 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) | |||
| return | |||
| } | |||
| @@ -181,10 +194,15 @@ const RunPanel: FC<RunProps> = ({ hideResult, activeTab = 'RESULT', runID, getRe | |||
| const [iterationRunResult, setIterationRunResult] = useState<NodeTracing[][]>([]) | |||
| const [iterDurationMap, setIterDurationMap] = useState<IterationDurationMap>({}) | |||
| const [retryRunResult, setRetryRunResult] = useState<NodeTracing[]>([]) | |||
| const [isShowIterationDetail, { | |||
| setTrue: doShowIterationDetail, | |||
| setFalse: doHideIterationDetail, | |||
| }] = useBoolean(false) | |||
| const [isShowRetryDetail, { | |||
| setTrue: doShowRetryDetail, | |||
| setFalse: doHideRetryDetail, | |||
| }] = useBoolean(false) | |||
| const handleShowIterationDetail = useCallback((detail: NodeTracing[][], iterDurationMap: IterationDurationMap) => { | |||
| setIterationRunResult(detail) | |||
| @@ -192,6 +210,11 @@ const RunPanel: FC<RunProps> = ({ hideResult, activeTab = 'RESULT', runID, getRe | |||
| setIterDurationMap(iterDurationMap) | |||
| }, [doShowIterationDetail, setIterationRunResult, setIterDurationMap]) | |||
| const handleShowRetryDetail = useCallback((detail: NodeTracing[]) => { | |||
| setRetryRunResult(detail) | |||
| doShowRetryDetail() | |||
| }, [doShowRetryDetail, setRetryRunResult]) | |||
| if (isShowIterationDetail) { | |||
| return ( | |||
| <div className='grow relative flex flex-col'> | |||
| @@ -261,13 +284,22 @@ const RunPanel: FC<RunProps> = ({ hideResult, activeTab = 'RESULT', runID, getRe | |||
| exceptionCounts={runDetail.exceptions_count} | |||
| /> | |||
| )} | |||
| {!loading && currentTab === 'TRACING' && ( | |||
| {!loading && currentTab === 'TRACING' && !isShowRetryDetail && ( | |||
| <TracingPanel | |||
| className='bg-background-section-burn' | |||
| list={list} | |||
| onShowIterationDetail={handleShowIterationDetail} | |||
| onShowRetryDetail={handleShowRetryDetail} | |||
| /> | |||
| )} | |||
| { | |||
| !loading && currentTab === 'TRACING' && isShowRetryDetail && ( | |||
| <RetryResultPanel | |||
| list={retryRunResult} | |||
| onBack={doHideRetryDetail} | |||
| /> | |||
| ) | |||
| } | |||
| </div> | |||
| </div> | |||
| ) | |||
| @@ -8,6 +8,7 @@ import { | |||
| RiCheckboxCircleFill, | |||
| RiErrorWarningLine, | |||
| RiLoader2Line, | |||
| RiRestartFill, | |||
| } from '@remixicon/react' | |||
| import BlockIcon from '../block-icon' | |||
| import { BlockEnum } from '../types' | |||
| @@ -20,6 +21,7 @@ import Button from '@/app/components/base/button' | |||
| import { CodeLanguage } from '@/app/components/workflow/nodes/code/types' | |||
| import type { IterationDurationMap, NodeTracing } from '@/types/workflow' | |||
| import ErrorHandleTip from '@/app/components/workflow/nodes/_base/components/error-handle/error-handle-tip' | |||
| import { hasRetryNode } from '@/app/components/workflow/utils' | |||
| type Props = { | |||
| className?: string | |||
| @@ -28,8 +30,10 @@ type Props = { | |||
| hideInfo?: boolean | |||
| hideProcessDetail?: boolean | |||
| onShowIterationDetail?: (detail: NodeTracing[][], iterDurationMap: IterationDurationMap) => void | |||
| onShowRetryDetail?: (detail: NodeTracing[]) => void | |||
| notShowIterationNav?: boolean | |||
| justShowIterationNavArrow?: boolean | |||
| justShowRetryNavArrow?: boolean | |||
| } | |||
| const NodePanel: FC<Props> = ({ | |||
| @@ -39,6 +43,7 @@ const NodePanel: FC<Props> = ({ | |||
| hideInfo = false, | |||
| hideProcessDetail, | |||
| onShowIterationDetail, | |||
| onShowRetryDetail, | |||
| notShowIterationNav, | |||
| justShowIterationNavArrow, | |||
| }) => { | |||
| @@ -88,11 +93,17 @@ const NodePanel: FC<Props> = ({ | |||
| }, [nodeInfo.expand, setCollapseState]) | |||
| const isIterationNode = nodeInfo.node_type === BlockEnum.Iteration | |||
| const isRetryNode = hasRetryNode(nodeInfo.node_type) && nodeInfo.retryDetail | |||
| const handleOnShowIterationDetail = (e: React.MouseEvent<HTMLButtonElement>) => { | |||
| e.stopPropagation() | |||
| e.nativeEvent.stopImmediatePropagation() | |||
| 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 ( | |||
| <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'> | |||
| @@ -169,6 +180,19 @@ const NodePanel: FC<Props> = ({ | |||
| <Split className='mt-2' /> | |||
| </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')}> | |||
| {(nodeInfo.status === 'stopped') && ( | |||
| <StatusContainer status='stopped'> | |||
| @@ -1,11 +1,17 @@ | |||
| 'use client' | |||
| import type { FC } from 'react' | |||
| import { useTranslation } from 'react-i18next' | |||
| import { | |||
| RiArrowRightSLine, | |||
| RiRestartFill, | |||
| } from '@remixicon/react' | |||
| import StatusPanel from './status' | |||
| import MetaData from './meta' | |||
| import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor' | |||
| import { CodeLanguage } from '@/app/components/workflow/nodes/code/types' | |||
| 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 = { | |||
| inputs?: string | |||
| @@ -22,6 +28,8 @@ type ResultPanelProps = { | |||
| showSteps?: boolean | |||
| exceptionCounts?: number | |||
| execution_metadata?: any | |||
| retry_events?: NodeTracing[] | |||
| onShowRetryDetail?: (retries: NodeTracing[]) => void | |||
| } | |||
| const ResultPanel: FC<ResultPanelProps> = ({ | |||
| @@ -38,8 +46,11 @@ const ResultPanel: FC<ResultPanelProps> = ({ | |||
| showSteps, | |||
| exceptionCounts, | |||
| execution_metadata, | |||
| retry_events, | |||
| onShowRetryDetail, | |||
| }) => { | |||
| const { t } = useTranslation() | |||
| return ( | |||
| <div className='bg-components-panel-bg py-2'> | |||
| <div className='px-4 py-2'> | |||
| @@ -51,6 +62,23 @@ const ResultPanel: FC<ResultPanelProps> = ({ | |||
| exceptionCounts={exceptionCounts} | |||
| /> | |||
| </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'> | |||
| <CodeEditor | |||
| readOnly | |||
| @@ -0,0 +1,46 @@ | |||
| '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) | |||
| @@ -21,6 +21,7 @@ import type { IterationDurationMap, NodeTracing } from '@/types/workflow' | |||
| type TracingPanelProps = { | |||
| list: NodeTracing[] | |||
| onShowIterationDetail?: (detail: NodeTracing[][], iterDurationMap: IterationDurationMap) => void | |||
| onShowRetryDetail?: (detail: NodeTracing[]) => void | |||
| className?: string | |||
| hideNodeInfo?: boolean | |||
| hideNodeProcessDetail?: boolean | |||
| @@ -160,6 +161,7 @@ function buildLogTree(nodes: NodeTracing[], t: (key: string) => string): Tracing | |||
| const TracingPanel: FC<TracingPanelProps> = ({ | |||
| list, | |||
| onShowIterationDetail, | |||
| onShowRetryDetail, | |||
| className, | |||
| hideNodeInfo = false, | |||
| hideNodeProcessDetail = false, | |||
| @@ -251,7 +253,9 @@ const TracingPanel: FC<TracingPanelProps> = ({ | |||
| <NodePanel | |||
| nodeInfo={node.data!} | |||
| onShowIterationDetail={onShowIterationDetail} | |||
| onShowRetryDetail={onShowRetryDetail} | |||
| justShowIterationNavArrow={true} | |||
| justShowRetryNavArrow={true} | |||
| hideInfo={hideNodeInfo} | |||
| hideProcessDetail={hideNodeProcessDetail} | |||
| /> | |||
| @@ -13,6 +13,7 @@ import type { | |||
| DefaultValueForm, | |||
| ErrorHandleTypeEnum, | |||
| } 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 { | |||
| Start = 'start', | |||
| @@ -68,6 +69,7 @@ export type CommonNodeType<T = {}> = { | |||
| _iterationIndex?: number | |||
| _inParallelHovering?: boolean | |||
| _waitingRun?: boolean | |||
| _retryIndex?: number | |||
| isInIteration?: boolean | |||
| iteration_id?: string | |||
| selected?: boolean | |||
| @@ -77,6 +79,7 @@ export type CommonNodeType<T = {}> = { | |||
| width?: number | |||
| height?: number | |||
| error_strategy?: ErrorHandleTypeEnum | |||
| retry_config?: WorkflowRetryConfig | |||
| default_value?: DefaultValueForm[] | |||
| } & T & Partial<Pick<ToolDefaultValue, 'provider_id' | 'provider_type' | 'provider_name' | 'tool_name'>> | |||
| @@ -293,6 +296,7 @@ export enum NodeRunningStatus { | |||
| Succeeded = 'succeeded', | |||
| Failed = 'failed', | |||
| Exception = 'exception', | |||
| Retry = 'retry', | |||
| } | |||
| export type OnNodeAdd = ( | |||
| @@ -26,6 +26,8 @@ import { | |||
| } from './types' | |||
| import { | |||
| CUSTOM_NODE, | |||
| DEFAULT_RETRY_INTERVAL, | |||
| DEFAULT_RETRY_MAX, | |||
| ITERATION_CHILDREN_Z_INDEX, | |||
| ITERATION_NODE_Z_INDEX, | |||
| NODE_WIDTH_X_OFFSET, | |||
| @@ -279,6 +281,14 @@ export const initialNodes = (originNodes: Node[], originEdges: Edge[]) => { | |||
| 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 | |||
| }) | |||
| } | |||
| @@ -797,3 +807,7 @@ export const isExceptionVariable = (variable: string, nodeType?: BlockEnum) => { | |||
| return false | |||
| } | |||
| export const hasRetryNode = (nodeType?: BlockEnum) => { | |||
| return nodeType === BlockEnum.LLM || nodeType === BlockEnum.Tool || nodeType === BlockEnum.HttpRequest || nodeType === BlockEnum.Code | |||
| } | |||
| @@ -329,6 +329,20 @@ const translation = { | |||
| 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: { | |||
| required: 'required', | |||
| @@ -329,6 +329,20 @@ const translation = { | |||
| tip: '流程中有 {{num}} 个节点运行异常,请前往追踪查看日志。', | |||
| }, | |||
| }, | |||
| retry: { | |||
| retry: '重试', | |||
| retryOnFailure: '失败时重试', | |||
| maxRetries: '最大重试次数', | |||
| retryInterval: '重试间隔', | |||
| retryTimes: '失败时重试 {{times}} 次', | |||
| retrying: '重试中...', | |||
| retrySuccessful: '重试成功', | |||
| retryFailed: '重试失败', | |||
| retryFailedTimes: '{{times}} 次重试失败', | |||
| times: '次', | |||
| ms: '毫秒', | |||
| retries: '{{num}} 重试次数', | |||
| }, | |||
| }, | |||
| start: { | |||
| required: '必填', | |||
| @@ -62,6 +62,7 @@ export type IOnNodeStarted = (nodeStarted: NodeStartedResponse) => void | |||
| export type IOnNodeFinished = (nodeFinished: NodeFinishedResponse) => void | |||
| export type IOnIterationStarted = (workflowStarted: IterationStartedResponse) => void | |||
| export type IOnIterationNext = (workflowStarted: IterationNextResponse) => void | |||
| export type IOnNodeRetry = (nodeFinished: NodeFinishedResponse) => void | |||
| export type IOnIterationFinished = (workflowFinished: IterationFinishedResponse) => void | |||
| export type IOnParallelBranchStarted = (parallelBranchStarted: ParallelBranchStartedResponse) => void | |||
| export type IOnParallelBranchFinished = (parallelBranchFinished: ParallelBranchFinishedResponse) => void | |||
| @@ -92,6 +93,7 @@ export type IOtherOptions = { | |||
| onIterationStart?: IOnIterationStarted | |||
| onIterationNext?: IOnIterationNext | |||
| onIterationFinish?: IOnIterationFinished | |||
| onNodeRetry?: IOnNodeRetry | |||
| onParallelBranchStarted?: IOnParallelBranchStarted | |||
| onParallelBranchFinished?: IOnParallelBranchFinished | |||
| onTextChunk?: IOnTextChunk | |||
| @@ -165,6 +167,7 @@ const handleStream = ( | |||
| onIterationStart?: IOnIterationStarted, | |||
| onIterationNext?: IOnIterationNext, | |||
| onIterationFinish?: IOnIterationFinished, | |||
| onNodeRetry?: IOnNodeRetry, | |||
| onParallelBranchStarted?: IOnParallelBranchStarted, | |||
| onParallelBranchFinished?: IOnParallelBranchFinished, | |||
| onTextChunk?: IOnTextChunk, | |||
| @@ -256,6 +259,9 @@ const handleStream = ( | |||
| else if (bufferObj.event === 'iteration_completed') { | |||
| onIterationFinish?.(bufferObj as IterationFinishedResponse) | |||
| } | |||
| else if (bufferObj.event === 'node_retry') { | |||
| onNodeRetry?.(bufferObj as NodeFinishedResponse) | |||
| } | |||
| else if (bufferObj.event === 'parallel_branch_started') { | |||
| onParallelBranchStarted?.(bufferObj as ParallelBranchStartedResponse) | |||
| } | |||
| @@ -462,6 +468,7 @@ export const ssePost = ( | |||
| onIterationStart, | |||
| onIterationNext, | |||
| onIterationFinish, | |||
| onNodeRetry, | |||
| onParallelBranchStarted, | |||
| onParallelBranchFinished, | |||
| onTextChunk, | |||
| @@ -533,7 +540,7 @@ export const ssePost = ( | |||
| return | |||
| } | |||
| 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) => { | |||
| 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 }) | |||
| @@ -52,10 +52,12 @@ export type NodeTracing = { | |||
| extras?: any | |||
| expand?: boolean // for UI | |||
| details?: NodeTracing[][] // iteration detail | |||
| retryDetail?: NodeTracing[] // retry detail | |||
| parallel_id?: string | |||
| parallel_start_node_id?: string | |||
| parent_parallel_id?: string | |||
| parent_parallel_start_node_id?: string | |||
| retry_index?: number | |||
| } | |||
| export type FetchWorkflowDraftResponse = { | |||
| @@ -178,6 +180,7 @@ export type NodeFinishedResponse = { | |||
| } | |||
| created_at: number | |||
| files?: FileResponse[] | |||
| retry_index?: number | |||
| } | |||
| } | |||