feat: last run frontend (#21369)
The frontend of feat: Persist Variables for Enhanced Debugging Workflow (#20699). Co-authored-by: jZonG <jzongcode@gmail.com>
This commit is contained in:
@@ -95,6 +95,7 @@ const FormItem: FC<Props> = ({
|
||||
const isArrayLikeType = [InputVarType.contexts, InputVarType.iterator].includes(type)
|
||||
const isContext = type === InputVarType.contexts
|
||||
const isIterator = type === InputVarType.iterator
|
||||
const isIteratorItemFile = isIterator && payload.isFileItem
|
||||
const singleFileValue = useMemo(() => {
|
||||
if (payload.variable === '#files#')
|
||||
return value?.[0] || []
|
||||
@@ -202,12 +203,12 @@ const FormItem: FC<Props> = ({
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{(type === InputVarType.multiFiles) && (
|
||||
{(type === InputVarType.multiFiles || isIteratorItemFile) && (
|
||||
<FileUploaderInAttachmentWrapper
|
||||
value={value}
|
||||
onChange={files => onChange(files)}
|
||||
fileConfig={{
|
||||
allowed_file_types: inStepRun
|
||||
allowed_file_types: (inStepRun || isIteratorItemFile)
|
||||
? [
|
||||
SupportUploadFileTypes.image,
|
||||
SupportUploadFileTypes.document,
|
||||
@@ -215,7 +216,7 @@ const FormItem: FC<Props> = ({
|
||||
SupportUploadFileTypes.video,
|
||||
]
|
||||
: payload.allowed_file_types,
|
||||
allowed_file_extensions: inStepRun
|
||||
allowed_file_extensions: (inStepRun || isIteratorItemFile)
|
||||
? [
|
||||
...FILE_EXTS[SupportUploadFileTypes.image],
|
||||
...FILE_EXTS[SupportUploadFileTypes.document],
|
||||
@@ -223,8 +224,8 @@ const FormItem: FC<Props> = ({
|
||||
...FILE_EXTS[SupportUploadFileTypes.video],
|
||||
]
|
||||
: payload.allowed_file_extensions,
|
||||
allowed_file_upload_methods: inStepRun ? [TransferMethod.local_file, TransferMethod.remote_url] : payload.allowed_file_upload_methods,
|
||||
number_limits: inStepRun ? 5 : payload.max_length,
|
||||
allowed_file_upload_methods: (inStepRun || isIteratorItemFile) ? [TransferMethod.local_file, TransferMethod.remote_url] : payload.allowed_file_upload_methods,
|
||||
number_limits: (inStepRun || isIteratorItemFile) ? 5 : payload.max_length,
|
||||
fileUploadConfig: fileSettings?.fileUploadConfig,
|
||||
}}
|
||||
/>
|
||||
@@ -272,7 +273,7 @@ const FormItem: FC<Props> = ({
|
||||
}
|
||||
|
||||
{
|
||||
isIterator && (
|
||||
(isIterator && !isIteratorItemFile) && (
|
||||
<div className='space-y-2'>
|
||||
{(value || []).map((item: any, index: number) => (
|
||||
<TextEditor
|
||||
|
@@ -61,10 +61,14 @@ const Form: FC<Props> = ({
|
||||
}
|
||||
}, [valuesRef, onChange, mapKeysWithSameValueSelector])
|
||||
const isArrayLikeType = [InputVarType.contexts, InputVarType.iterator].includes(inputs[0]?.type)
|
||||
const isIteratorItemFile = inputs[0]?.type === InputVarType.iterator && inputs[0]?.isFileItem
|
||||
|
||||
const isContext = inputs[0]?.type === InputVarType.contexts
|
||||
const handleAddContext = useCallback(() => {
|
||||
const newValues = produce(values, (draft: any) => {
|
||||
const key = inputs[0].variable
|
||||
if (!draft[key])
|
||||
draft[key] = []
|
||||
draft[key].push(isContext ? RETRIEVAL_OUTPUT_STRUCT : '')
|
||||
})
|
||||
onChange(newValues)
|
||||
@@ -75,7 +79,7 @@ const Form: FC<Props> = ({
|
||||
{label && (
|
||||
<div className='mb-1 flex items-center justify-between'>
|
||||
<div className='system-xs-medium-uppercase flex h-6 items-center text-text-tertiary'>{label}</div>
|
||||
{isArrayLikeType && (
|
||||
{isArrayLikeType && !isIteratorItemFile && (
|
||||
<AddButton onClick={handleAddContext} />
|
||||
)}
|
||||
</div>
|
||||
|
@@ -1,30 +1,23 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiCloseLine,
|
||||
RiLoader2Line,
|
||||
} from '@remixicon/react'
|
||||
import type { Props as FormProps } from './form'
|
||||
import Form from './form'
|
||||
import cn from '@/utils/classnames'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { StopCircle } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices'
|
||||
import Split from '@/app/components/workflow/nodes/_base/components/split'
|
||||
import { InputVarType, NodeRunningStatus } from '@/app/components/workflow/types'
|
||||
import ResultPanel from '@/app/components/workflow/run/result-panel'
|
||||
import { InputVarType } from '@/app/components/workflow/types'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { TransferMethod } from '@/types/app'
|
||||
import { getProcessedFiles } from '@/app/components/base/file-uploader/utils'
|
||||
import type { BlockEnum } from '@/app/components/workflow/types'
|
||||
import type { BlockEnum, NodeRunningStatus } from '@/app/components/workflow/types'
|
||||
import type { Emoji } from '@/app/components/tools/types'
|
||||
import type { SpecialResultPanelProps } from '@/app/components/workflow/run/special-result-panel'
|
||||
import SpecialResultPanel from '@/app/components/workflow/run/special-result-panel'
|
||||
|
||||
import PanelWrap from './panel-wrap'
|
||||
const i18nPrefix = 'workflow.singleRun'
|
||||
|
||||
type BeforeRunFormProps = {
|
||||
export type BeforeRunFormProps = {
|
||||
nodeName: string
|
||||
nodeType?: BlockEnum
|
||||
toolIcon?: string | Emoji
|
||||
@@ -32,12 +25,15 @@ type BeforeRunFormProps = {
|
||||
onRun: (submitData: Record<string, any>) => void
|
||||
onStop: () => void
|
||||
runningStatus: NodeRunningStatus
|
||||
result?: React.JSX.Element
|
||||
forms: FormProps[]
|
||||
showSpecialResultPanel?: boolean
|
||||
existVarValuesInForms: Record<string, any>[]
|
||||
filteredExistVarForms: FormProps[]
|
||||
} & Partial<SpecialResultPanelProps>
|
||||
|
||||
function formatValue(value: string | any, type: InputVarType) {
|
||||
if(value === undefined || value === null)
|
||||
return value
|
||||
if (type === InputVarType.number)
|
||||
return Number.parseFloat(value)
|
||||
if (type === InputVarType.json)
|
||||
@@ -53,6 +49,8 @@ function formatValue(value: string | any, type: InputVarType) {
|
||||
if (type === InputVarType.singleFile) {
|
||||
if (Array.isArray(value))
|
||||
return getProcessedFiles(value)
|
||||
if (!value)
|
||||
return undefined
|
||||
return getProcessedFiles([value])[0]
|
||||
}
|
||||
|
||||
@@ -60,22 +58,17 @@ function formatValue(value: string | any, type: InputVarType) {
|
||||
}
|
||||
const BeforeRunForm: FC<BeforeRunFormProps> = ({
|
||||
nodeName,
|
||||
nodeType,
|
||||
toolIcon,
|
||||
onHide,
|
||||
onRun,
|
||||
onStop,
|
||||
runningStatus,
|
||||
result,
|
||||
forms,
|
||||
showSpecialResultPanel,
|
||||
...restResultPanelParams
|
||||
filteredExistVarForms,
|
||||
existVarValuesInForms,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const isFinished = runningStatus === NodeRunningStatus.Succeeded || runningStatus === NodeRunningStatus.Failed || runningStatus === NodeRunningStatus.Exception
|
||||
const isRunning = runningStatus === NodeRunningStatus.Running
|
||||
const isFileLoaded = (() => {
|
||||
if (!forms || forms.length === 0)
|
||||
return true
|
||||
// system files
|
||||
const filesForm = forms.find(item => !!item.values['#files#'])
|
||||
if (!filesForm)
|
||||
@@ -87,12 +80,14 @@ const BeforeRunForm: FC<BeforeRunFormProps> = ({
|
||||
|
||||
return true
|
||||
})()
|
||||
const handleRun = useCallback(() => {
|
||||
const handleRun = () => {
|
||||
let errMsg = ''
|
||||
forms.forEach((form) => {
|
||||
forms.forEach((form, i) => {
|
||||
const existVarValuesInForm = existVarValuesInForms[i]
|
||||
|
||||
form.inputs.forEach((input) => {
|
||||
const value = form.values[input.variable] as any
|
||||
if (!errMsg && input.required && (value === '' || value === undefined || value === null || (input.type === InputVarType.files && value.length === 0)))
|
||||
if (!errMsg && input.required && !(input.variable in existVarValuesInForm) && (value === '' || value === undefined || value === null || (input.type === InputVarType.files && value.length === 0)))
|
||||
errMsg = t('workflow.errorMsg.fieldRequired', { field: typeof input.label === 'object' ? input.label.variable : input.label })
|
||||
|
||||
if (!errMsg && (input.type === InputVarType.singleFile || input.type === InputVarType.multiFiles) && value) {
|
||||
@@ -137,69 +132,45 @@ const BeforeRunForm: FC<BeforeRunFormProps> = ({
|
||||
}
|
||||
|
||||
onRun(submitData)
|
||||
}, [forms, onRun, t])
|
||||
}
|
||||
const hasRun = useRef(false)
|
||||
useEffect(() => {
|
||||
// React 18 run twice in dev mode
|
||||
if(hasRun.current)
|
||||
return
|
||||
hasRun.current = true
|
||||
if(filteredExistVarForms.length === 0)
|
||||
onRun({})
|
||||
}, [filteredExistVarForms, onRun])
|
||||
|
||||
if(filteredExistVarForms.length === 0)
|
||||
return null
|
||||
|
||||
return (
|
||||
<div className='absolute inset-0 z-10 rounded-2xl bg-background-overlay-alt pt-10'>
|
||||
<div className='flex h-full flex-col rounded-2xl bg-components-panel-bg'>
|
||||
<div className='flex h-8 shrink-0 items-center justify-between pl-4 pr-3 pt-3'>
|
||||
<div className='truncate text-base font-semibold text-text-primary'>
|
||||
{t(`${i18nPrefix}.testRun`)} {nodeName}
|
||||
</div>
|
||||
<div className='ml-2 shrink-0 cursor-pointer p-1' onClick={() => {
|
||||
onHide()
|
||||
}}>
|
||||
<RiCloseLine className='h-4 w-4 text-text-tertiary ' />
|
||||
</div>
|
||||
<PanelWrap
|
||||
nodeName={nodeName}
|
||||
onHide={onHide}
|
||||
>
|
||||
<div className='h-0 grow overflow-y-auto pb-4'>
|
||||
<div className='mt-3 space-y-4 px-4'>
|
||||
{filteredExistVarForms.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 className='mt-4 flex justify-between space-x-2 px-4' >
|
||||
<Button disabled={!isFileLoaded} variant='primary' className='w-0 grow space-x-2' onClick={handleRun}>
|
||||
<div>{t(`${i18nPrefix}.startRun`)}</div>
|
||||
</Button>
|
||||
</div>
|
||||
{
|
||||
showSpecialResultPanel && (
|
||||
<div className='h-0 grow overflow-y-auto pb-4'>
|
||||
<SpecialResultPanel {...restResultPanelParams} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
!showSpecialResultPanel && (
|
||||
<div className='h-0 grow overflow-y-auto pb-4'>
|
||||
<div className='mt-3 space-y-4 px-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 className='mt-4 flex justify-between space-x-2 px-4' >
|
||||
{isRunning && (
|
||||
<div
|
||||
className='cursor-pointer rounded-lg border border-divider-regular bg-components-button-secondary-bg p-2 shadow-xs'
|
||||
onClick={onStop}
|
||||
>
|
||||
<StopCircle className='h-4 w-4 text-text-tertiary' />
|
||||
</div>
|
||||
)}
|
||||
<Button disabled={!isFileLoaded || isRunning} variant='primary' className='w-0 grow space-x-2' onClick={handleRun}>
|
||||
{isRunning && <RiLoader2Line className='h-4 w-4 animate-spin' />}
|
||||
<div>{t(`${i18nPrefix}.${isRunning ? 'running' : 'startRun'}`)}</div>
|
||||
</Button>
|
||||
</div>
|
||||
{isRunning && (
|
||||
<ResultPanel status='running' showSteps={false} />
|
||||
)}
|
||||
{isFinished && (
|
||||
<>
|
||||
{result}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</PanelWrap>
|
||||
)
|
||||
}
|
||||
export default React.memo(BeforeRunForm)
|
||||
|
@@ -0,0 +1,41 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiCloseLine,
|
||||
} from '@remixicon/react'
|
||||
|
||||
const i18nPrefix = 'workflow.singleRun'
|
||||
|
||||
export type Props = {
|
||||
nodeName: string
|
||||
onHide: () => void
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const PanelWrap: FC<Props> = ({
|
||||
nodeName,
|
||||
onHide,
|
||||
children,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div className='absolute inset-0 z-10 rounded-2xl bg-background-overlay-alt'>
|
||||
<div className='flex h-full flex-col rounded-2xl bg-components-panel-bg'>
|
||||
<div className='flex h-8 shrink-0 items-center justify-between pl-4 pr-3 pt-3'>
|
||||
<div className='truncate text-base font-semibold text-text-primary'>
|
||||
{t(`${i18nPrefix}.testRun`)} {nodeName}
|
||||
</div>
|
||||
<div className='ml-2 shrink-0 cursor-pointer p-1' onClick={() => {
|
||||
onHide()
|
||||
}}>
|
||||
<RiCloseLine className='h-4 w-4 text-text-tertiary ' />
|
||||
</div>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(PanelWrap)
|
@@ -13,7 +13,7 @@ import {
|
||||
useNodesInteractions,
|
||||
useNodesSyncDraft,
|
||||
} from '../../../hooks'
|
||||
import type { Node } from '../../../types'
|
||||
import { type Node, NodeRunningStatus } from '../../../types'
|
||||
import { canRunBySingle } from '../../../utils'
|
||||
import PanelOperator from './panel-operator'
|
||||
import {
|
||||
@@ -31,11 +31,12 @@ const NodeControl: FC<NodeControlProps> = ({
|
||||
const { handleNodeDataUpdate } = useNodeDataUpdate()
|
||||
const { handleNodeSelect } = useNodesInteractions()
|
||||
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
|
||||
const isSingleRunning = data._singleRunningStatus === NodeRunningStatus.Running
|
||||
const handleOpenChange = useCallback((newOpen: boolean) => {
|
||||
setOpen(newOpen)
|
||||
}, [])
|
||||
|
||||
const isChildNode = !!(data.isInIteration || data.isInLoop)
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
@@ -49,23 +50,25 @@ const NodeControl: FC<NodeControlProps> = ({
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
{
|
||||
canRunBySingle(data.type) && (
|
||||
canRunBySingle(data.type, isChildNode) && (
|
||||
<div
|
||||
className='flex h-5 w-5 cursor-pointer items-center justify-center rounded-md hover:bg-state-base-hover'
|
||||
onClick={() => {
|
||||
const nextData: Record<string, any> = {
|
||||
_isSingleRun: !isSingleRunning,
|
||||
}
|
||||
if(isSingleRunning)
|
||||
nextData._singleRunningStatus = undefined
|
||||
|
||||
handleNodeDataUpdate({
|
||||
id,
|
||||
data: {
|
||||
_isSingleRun: !data._isSingleRun,
|
||||
},
|
||||
data: nextData,
|
||||
})
|
||||
handleNodeSelect(id)
|
||||
if (!data._isSingleRun)
|
||||
handleSyncWorkflowDraft(true)
|
||||
}}
|
||||
>
|
||||
{
|
||||
data._isSingleRun
|
||||
isSingleRunning
|
||||
? <Stop className='h-3 w-3' />
|
||||
: (
|
||||
<Tooltip
|
||||
|
@@ -83,14 +83,16 @@ const PanelOperatorPopup = ({
|
||||
|
||||
const link = useNodeHelpLink(data.type)
|
||||
|
||||
const isChildNode = !!(data.isInIteration || data.isInLoop)
|
||||
|
||||
return (
|
||||
<div className='w-[240px] rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-xl'>
|
||||
{
|
||||
(showChangeBlock || canRunBySingle(data.type)) && (
|
||||
(showChangeBlock || canRunBySingle(data.type, isChildNode)) && (
|
||||
<>
|
||||
<div className='p-1'>
|
||||
{
|
||||
canRunBySingle(data.type) && (
|
||||
canRunBySingle(data.type, isChildNode) && (
|
||||
<div
|
||||
className={`
|
||||
flex h-8 cursor-pointer items-center rounded-lg px-3 text-sm text-text-secondary
|
||||
|
@@ -0,0 +1,429 @@
|
||||
import type {
|
||||
FC,
|
||||
ReactNode,
|
||||
} from 'react'
|
||||
import {
|
||||
cloneElement,
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import {
|
||||
RiCloseLine,
|
||||
RiPlayLargeLine,
|
||||
} from '@remixicon/react'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import NextStep from '../next-step'
|
||||
import PanelOperator from '../panel-operator'
|
||||
import NodePosition from '@/app/components/workflow/nodes/_base/components/node-position'
|
||||
import HelpLink from '../help-link'
|
||||
import {
|
||||
DescriptionInput,
|
||||
TitleInput,
|
||||
} from '../title-description-input'
|
||||
import ErrorHandleOnPanel from '../error-handle/error-handle-on-panel'
|
||||
import RetryOnPanel from '../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,
|
||||
useNodeDataUpdate,
|
||||
useNodesInteractions,
|
||||
useNodesReadOnly,
|
||||
useToolIcon,
|
||||
useWorkflowHistory,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
import {
|
||||
canRunBySingle,
|
||||
hasErrorHandleNode,
|
||||
hasRetryNode,
|
||||
} from '@/app/components/workflow/utils'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { BlockEnum, type Node, NodeRunningStatus } from '@/app/components/workflow/types'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import Tab, { TabType } from './tab'
|
||||
import LastRun from './last-run'
|
||||
import useLastRun from './last-run/use-last-run'
|
||||
import BeforeRunForm from '../before-run-form'
|
||||
import { debounce } from 'lodash-es'
|
||||
import { NODES_EXTRA_DATA } from '@/app/components/workflow/constants'
|
||||
import { useLogs } from '@/app/components/workflow/run/hooks'
|
||||
import PanelWrap from '../before-run-form/panel-wrap'
|
||||
import SpecialResultPanel from '@/app/components/workflow/run/special-result-panel'
|
||||
import { Stop } from '@/app/components/base/icons/src/vender/line/mediaAndDevices'
|
||||
|
||||
type BasePanelProps = {
|
||||
children: ReactNode
|
||||
} & Node
|
||||
|
||||
const BasePanel: FC<BasePanelProps> = ({
|
||||
id,
|
||||
data,
|
||||
children,
|
||||
position,
|
||||
width,
|
||||
height,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { showMessageLogModal } = useAppStore(useShallow(state => ({
|
||||
showMessageLogModal: state.showMessageLogModal,
|
||||
})))
|
||||
const isSingleRunning = data._singleRunningStatus === NodeRunningStatus.Running
|
||||
|
||||
const showSingleRunPanel = useStore(s => s.showSingleRunPanel)
|
||||
const workflowCanvasWidth = useStore(s => s.workflowCanvasWidth)
|
||||
const nodePanelWidth = useStore(s => s.nodePanelWidth)
|
||||
const otherPanelWidth = useStore(s => s.otherPanelWidth)
|
||||
const setNodePanelWidth = useStore(s => s.setNodePanelWidth)
|
||||
|
||||
const maxNodePanelWidth = useMemo(() => {
|
||||
if (!workflowCanvasWidth)
|
||||
return 720
|
||||
if (!otherPanelWidth)
|
||||
return workflowCanvasWidth - 400
|
||||
|
||||
return workflowCanvasWidth - otherPanelWidth - 400
|
||||
}, [workflowCanvasWidth, otherPanelWidth])
|
||||
|
||||
const updateNodePanelWidth = useCallback((width: number) => {
|
||||
// Ensure the width is within the min and max range
|
||||
const newValue = Math.min(Math.max(width, 400), maxNodePanelWidth)
|
||||
localStorage.setItem('workflow-node-panel-width', `${newValue}`)
|
||||
setNodePanelWidth(newValue)
|
||||
}, [maxNodePanelWidth, setNodePanelWidth])
|
||||
|
||||
const handleResize = useCallback((width: number) => {
|
||||
updateNodePanelWidth(width)
|
||||
}, [updateNodePanelWidth])
|
||||
|
||||
const {
|
||||
triggerRef,
|
||||
containerRef,
|
||||
} = useResizePanel({
|
||||
direction: 'horizontal',
|
||||
triggerDirection: 'left',
|
||||
minWidth: 400,
|
||||
maxWidth: maxNodePanelWidth,
|
||||
onResize: debounce(handleResize),
|
||||
})
|
||||
|
||||
const debounceUpdate = debounce(updateNodePanelWidth)
|
||||
useEffect(() => {
|
||||
if (!workflowCanvasWidth)
|
||||
return
|
||||
if (workflowCanvasWidth - 400 <= nodePanelWidth + otherPanelWidth)
|
||||
debounceUpdate(workflowCanvasWidth - 400 - otherPanelWidth)
|
||||
}, [nodePanelWidth, otherPanelWidth, workflowCanvasWidth, updateNodePanelWidth])
|
||||
|
||||
const { handleNodeSelect } = useNodesInteractions()
|
||||
const { nodesReadOnly } = useNodesReadOnly()
|
||||
const { availableNextBlocks } = useAvailableBlocks(data.type, data.isInIteration, data.isInLoop)
|
||||
const toolIcon = useToolIcon(data)
|
||||
|
||||
const { saveStateToHistory } = useWorkflowHistory()
|
||||
|
||||
const {
|
||||
handleNodeDataUpdate,
|
||||
handleNodeDataUpdateWithSyncDraft,
|
||||
} = useNodeDataUpdate()
|
||||
|
||||
const handleTitleBlur = useCallback((title: string) => {
|
||||
handleNodeDataUpdateWithSyncDraft({ id, data: { title } })
|
||||
saveStateToHistory(WorkflowHistoryEvent.NodeTitleChange)
|
||||
}, [handleNodeDataUpdateWithSyncDraft, id, saveStateToHistory])
|
||||
const handleDescriptionChange = useCallback((desc: string) => {
|
||||
handleNodeDataUpdateWithSyncDraft({ id, data: { desc } })
|
||||
saveStateToHistory(WorkflowHistoryEvent.NodeDescriptionChange)
|
||||
}, [handleNodeDataUpdateWithSyncDraft, id, saveStateToHistory])
|
||||
|
||||
const isChildNode = !!(data.isInIteration || data.isInLoop)
|
||||
const isSupportSingleRun = canRunBySingle(data.type, isChildNode)
|
||||
const appDetail = useAppStore(state => state.appDetail)
|
||||
|
||||
const hasClickRunning = useRef(false)
|
||||
const [isPaused, setIsPaused] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if(data._singleRunningStatus === NodeRunningStatus.Running) {
|
||||
hasClickRunning.current = true
|
||||
setIsPaused(false)
|
||||
}
|
||||
else if(data._isSingleRun && data._singleRunningStatus === undefined && hasClickRunning) {
|
||||
setIsPaused(true)
|
||||
hasClickRunning.current = false
|
||||
}
|
||||
}, [data])
|
||||
|
||||
const updateNodeRunningStatus = useCallback((status: NodeRunningStatus) => {
|
||||
handleNodeDataUpdate({
|
||||
id,
|
||||
data: {
|
||||
...data,
|
||||
_singleRunningStatus: status,
|
||||
},
|
||||
})
|
||||
}, [handleNodeDataUpdate, id, data])
|
||||
|
||||
useEffect(() => {
|
||||
// console.log(`id changed: ${id}, hasClickRunning: ${hasClickRunning.current}`)
|
||||
hasClickRunning.current = false
|
||||
}, [id])
|
||||
|
||||
const {
|
||||
isShowSingleRun,
|
||||
hideSingleRun,
|
||||
runningStatus,
|
||||
handleStop,
|
||||
runInputData,
|
||||
runInputDataRef,
|
||||
runResult,
|
||||
getInputVars,
|
||||
toVarInputs,
|
||||
tabType,
|
||||
isRunAfterSingleRun,
|
||||
setTabType,
|
||||
singleRunParams,
|
||||
nodeInfo,
|
||||
setRunInputData,
|
||||
handleSingleRun,
|
||||
handleRunWithParams,
|
||||
getExistVarValuesInForms,
|
||||
getFilteredExistVarForms,
|
||||
} = useLastRun<typeof data>({
|
||||
id,
|
||||
data,
|
||||
defaultRunInputData: NODES_EXTRA_DATA[data.type]?.defaultRunInputData || {},
|
||||
isPaused,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
setIsPaused(false)
|
||||
}, [tabType])
|
||||
|
||||
const logParams = useLogs()
|
||||
const passedLogParams = (() => {
|
||||
if ([BlockEnum.Tool, BlockEnum.Agent, BlockEnum.Iteration, BlockEnum.Loop].includes(data.type))
|
||||
return logParams
|
||||
|
||||
return {}
|
||||
})()
|
||||
|
||||
if(logParams.showSpecialResultPanel) {
|
||||
return (
|
||||
<div className={cn(
|
||||
'relative mr-1 h-full',
|
||||
)}>
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={cn('flex h-full flex-col rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg', showSingleRunPanel ? 'overflow-hidden' : 'overflow-y-auto')}
|
||||
style={{
|
||||
width: `${nodePanelWidth}px`,
|
||||
}}
|
||||
>
|
||||
<PanelWrap
|
||||
nodeName={data.title}
|
||||
onHide={hideSingleRun}
|
||||
>
|
||||
<div className='h-0 grow overflow-y-auto pb-4'>
|
||||
<SpecialResultPanel {...passedLogParams} />
|
||||
</div>
|
||||
</PanelWrap>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (isShowSingleRun) {
|
||||
return (
|
||||
<div className={cn(
|
||||
'relative mr-1 h-full',
|
||||
)}>
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={cn('flex h-full flex-col rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg', showSingleRunPanel ? 'overflow-hidden' : 'overflow-y-auto')}
|
||||
style={{
|
||||
width: `${nodePanelWidth}px`,
|
||||
}}
|
||||
>
|
||||
<BeforeRunForm
|
||||
nodeName={data.title}
|
||||
nodeType={data.type}
|
||||
onHide={hideSingleRun}
|
||||
onRun={handleRunWithParams}
|
||||
{...singleRunParams!}
|
||||
{...passedLogParams}
|
||||
existVarValuesInForms={getExistVarValuesInForms(singleRunParams?.forms as any)}
|
||||
filteredExistVarForms={getFilteredExistVarForms(singleRunParams?.forms as any)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn(
|
||||
'relative mr-1 h-full',
|
||||
showMessageLogModal && '!absolute -top-[5px] right-[416px] z-0 !mr-0 w-[384px] overflow-hidden rounded-2xl border-[0.5px] border-components-panel-border shadow-lg transition-all',
|
||||
)}>
|
||||
<div
|
||||
ref={triggerRef}
|
||||
className='absolute -left-1 top-0 flex h-full w-1 cursor-col-resize resize-x items-center justify-center'>
|
||||
<div className='h-10 w-0.5 rounded-sm bg-state-base-handle hover:h-full hover:bg-state-accent-solid active:h-full active:bg-state-accent-solid'></div>
|
||||
</div>
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={cn('flex h-full flex-col rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg', showSingleRunPanel ? 'overflow-hidden' : 'overflow-y-auto')}
|
||||
style={{
|
||||
width: `${nodePanelWidth}px`,
|
||||
}}
|
||||
>
|
||||
<div className='sticky top-0 z-10 shrink-0 border-b-[0.5px] border-divider-regular bg-components-panel-bg'>
|
||||
<div className='flex items-center px-4 pb-1 pt-4'>
|
||||
<BlockIcon
|
||||
className='mr-1 shrink-0'
|
||||
type={data.type}
|
||||
toolIcon={toolIcon}
|
||||
size='md'
|
||||
/>
|
||||
<TitleInput
|
||||
value={data.title || ''}
|
||||
onBlur={handleTitleBlur}
|
||||
/>
|
||||
<div className='flex shrink-0 items-center text-text-tertiary'>
|
||||
{
|
||||
isSupportSingleRun && !nodesReadOnly && (
|
||||
<Tooltip
|
||||
popupContent={t('workflow.panel.runThisStep')}
|
||||
popupClassName='mr-1'
|
||||
disabled={isSingleRunning}
|
||||
>
|
||||
<div
|
||||
className='mr-1 flex h-6 w-6 cursor-pointer items-center justify-center rounded-md hover:bg-state-base-hover'
|
||||
onClick={() => {
|
||||
if(isSingleRunning) {
|
||||
handleNodeDataUpdate({
|
||||
id,
|
||||
data: {
|
||||
_isSingleRun: false,
|
||||
_singleRunningStatus: undefined,
|
||||
},
|
||||
})
|
||||
}
|
||||
else {
|
||||
handleSingleRun()
|
||||
}
|
||||
}}
|
||||
>
|
||||
{
|
||||
isSingleRunning ? <Stop className='h-4 w-4 text-text-tertiary' />
|
||||
: <RiPlayLargeLine className='h-4 w-4 text-text-tertiary' />
|
||||
}
|
||||
</div>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
<NodePosition nodePosition={position} nodeWidth={width} nodeHeight={height}></NodePosition>
|
||||
<HelpLink nodeType={data.type} />
|
||||
<PanelOperator id={id} data={data} showHelpLink={false} />
|
||||
<div className='mx-3 h-3.5 w-[1px] bg-divider-regular' />
|
||||
<div
|
||||
className='flex h-6 w-6 cursor-pointer items-center justify-center'
|
||||
onClick={() => handleNodeSelect(id, true)}
|
||||
>
|
||||
<RiCloseLine className='h-4 w-4 text-text-tertiary' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='p-2'>
|
||||
<DescriptionInput
|
||||
value={data.desc || ''}
|
||||
onChange={handleDescriptionChange}
|
||||
/>
|
||||
</div>
|
||||
<div className='pl-4'>
|
||||
<Tab
|
||||
value={tabType}
|
||||
onChange={setTabType}
|
||||
/>
|
||||
</div>
|
||||
<Split />
|
||||
</div>
|
||||
|
||||
{tabType === TabType.settings && (
|
||||
<>
|
||||
<div>
|
||||
{cloneElement(children as any, {
|
||||
id,
|
||||
data,
|
||||
panelProps: {
|
||||
getInputVars,
|
||||
toVarInputs,
|
||||
runInputData,
|
||||
setRunInputData,
|
||||
runResult,
|
||||
runInputDataRef,
|
||||
},
|
||||
})}
|
||||
</div>
|
||||
<Split />
|
||||
{
|
||||
hasRetryNode(data.type) && (
|
||||
<RetryOnPanel
|
||||
id={id}
|
||||
data={data}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
hasErrorHandleNode(data.type) && (
|
||||
<ErrorHandleOnPanel
|
||||
id={id}
|
||||
data={data}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
!!availableNextBlocks.length && (
|
||||
<div className='border-t-[0.5px] border-divider-regular p-4'>
|
||||
<div className='system-sm-semibold-uppercase mb-1 flex items-center text-text-secondary'>
|
||||
{t('workflow.panel.nextStep').toLocaleUpperCase()}
|
||||
</div>
|
||||
<div className='system-xs-regular mb-2 text-text-tertiary'>
|
||||
{t('workflow.panel.addNextStep')}
|
||||
</div>
|
||||
<NextStep selectedNode={{ id, data } as Node} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)}
|
||||
|
||||
{tabType === TabType.lastRun && (
|
||||
<LastRun
|
||||
appId={appDetail?.id || ''}
|
||||
nodeId={id}
|
||||
canSingleRun={isSupportSingleRun}
|
||||
runningStatus={runningStatus}
|
||||
isRunAfterSingleRun={isRunAfterSingleRun}
|
||||
updateNodeRunningStatus={updateNodeRunningStatus}
|
||||
onSingleRunClicked={handleSingleRun}
|
||||
nodeInfo={nodeInfo}
|
||||
singleRunResult={runResult!}
|
||||
isPaused={isPaused}
|
||||
{...passedLogParams}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(BasePanel)
|
@@ -0,0 +1,126 @@
|
||||
'use client'
|
||||
import type { ResultPanelProps } from '@/app/components/workflow/run/result-panel'
|
||||
import ResultPanel from '@/app/components/workflow/run/result-panel'
|
||||
import { NodeRunningStatus } from '@/app/components/workflow/types'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import NoData from './no-data'
|
||||
import { useLastRun } from '@/service/use-workflow'
|
||||
import { RiLoader2Line } from '@remixicon/react'
|
||||
import type { NodeTracing } from '@/types/workflow'
|
||||
|
||||
type Props = {
|
||||
appId: string
|
||||
nodeId: string
|
||||
canSingleRun: boolean
|
||||
isRunAfterSingleRun: boolean
|
||||
updateNodeRunningStatus: (status: NodeRunningStatus) => void
|
||||
nodeInfo?: NodeTracing
|
||||
runningStatus?: NodeRunningStatus
|
||||
onSingleRunClicked: () => void
|
||||
singleRunResult?: NodeTracing
|
||||
isPaused?: boolean
|
||||
} & Partial<ResultPanelProps>
|
||||
|
||||
const LastRun: FC<Props> = ({
|
||||
appId,
|
||||
nodeId,
|
||||
canSingleRun,
|
||||
isRunAfterSingleRun,
|
||||
updateNodeRunningStatus,
|
||||
nodeInfo,
|
||||
runningStatus: oneStepRunRunningStatus,
|
||||
onSingleRunClicked,
|
||||
singleRunResult,
|
||||
isPaused,
|
||||
...otherResultPanelProps
|
||||
}) => {
|
||||
const isOneStepRunSucceed = oneStepRunRunningStatus === NodeRunningStatus.Succeeded
|
||||
const isOneStepRunFailed = oneStepRunRunningStatus === NodeRunningStatus.Failed
|
||||
// hide page and return to page would lost the oneStepRunRunningStatus
|
||||
const [hidePageOneStepFinishedStatus, setHidePageOneStepFinishedStatus] = React.useState<NodeRunningStatus | null>(null)
|
||||
const [pageHasHide, setPageHasHide] = useState(false)
|
||||
const [pageShowed, setPageShowed] = useState(false)
|
||||
|
||||
const hidePageOneStepRunFinished = [NodeRunningStatus.Succeeded, NodeRunningStatus.Failed].includes(hidePageOneStepFinishedStatus!)
|
||||
const canRunLastRun = !isRunAfterSingleRun || isOneStepRunSucceed || isOneStepRunFailed || (pageHasHide && hidePageOneStepRunFinished)
|
||||
const { data: lastRunResult, isFetching, error } = useLastRun(appId, nodeId, canRunLastRun)
|
||||
const isRunning = useMemo(() => {
|
||||
if(isPaused)
|
||||
return false
|
||||
|
||||
if(!isRunAfterSingleRun)
|
||||
return isFetching
|
||||
return [NodeRunningStatus.Running, NodeRunningStatus.NotStart].includes(oneStepRunRunningStatus!)
|
||||
}, [isFetching, isPaused, isRunAfterSingleRun, oneStepRunRunningStatus])
|
||||
|
||||
const noLastRun = (error as any)?.status === 404
|
||||
const runResult = (canRunLastRun ? lastRunResult : singleRunResult) || lastRunResult || {}
|
||||
|
||||
const resetHidePageStatus = useCallback(() => {
|
||||
setPageHasHide(false)
|
||||
setPageShowed(false)
|
||||
setHidePageOneStepFinishedStatus(null)
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
if (pageShowed && hidePageOneStepFinishedStatus && (!oneStepRunRunningStatus || oneStepRunRunningStatus === NodeRunningStatus.NotStart)) {
|
||||
updateNodeRunningStatus(hidePageOneStepFinishedStatus)
|
||||
resetHidePageStatus()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isOneStepRunSucceed, isOneStepRunFailed, oneStepRunRunningStatus])
|
||||
|
||||
useEffect(() => {
|
||||
if([NodeRunningStatus.Succeeded, NodeRunningStatus.Failed].includes(oneStepRunRunningStatus!))
|
||||
setHidePageOneStepFinishedStatus(oneStepRunRunningStatus!)
|
||||
}, [oneStepRunRunningStatus])
|
||||
|
||||
useEffect(() => {
|
||||
resetHidePageStatus()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [nodeId])
|
||||
|
||||
const handlePageVisibilityChange = useCallback(() => {
|
||||
if (document.visibilityState === 'hidden')
|
||||
setPageHasHide(true)
|
||||
else
|
||||
setPageShowed(true)
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
document.addEventListener('visibilitychange', handlePageVisibilityChange)
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('visibilitychange', handlePageVisibilityChange)
|
||||
}
|
||||
}, [handlePageVisibilityChange])
|
||||
|
||||
if (isFetching && !isRunAfterSingleRun) {
|
||||
return (
|
||||
<div className='flex h-0 grow flex-col items-center justify-center'>
|
||||
<RiLoader2Line className='size-4 animate-spin text-text-tertiary' />
|
||||
</div>)
|
||||
}
|
||||
|
||||
if (isRunning)
|
||||
return <ResultPanel status='running' showSteps={false} />
|
||||
|
||||
if (!isPaused && (noLastRun || !runResult)) {
|
||||
return (
|
||||
<NoData canSingleRun={canSingleRun} onSingleRun={onSingleRunClicked} />
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<ResultPanel
|
||||
{...runResult as any}
|
||||
{...otherResultPanelProps}
|
||||
status={isPaused ? NodeRunningStatus.Stopped : ((runResult as any).status || otherResultPanelProps.status)}
|
||||
total_tokens={(runResult as any)?.execution_metadata?.total_tokens || otherResultPanelProps?.total_tokens}
|
||||
created_by={(runResult as any)?.created_by_account?.created_by || otherResultPanelProps?.created_by}
|
||||
nodeInfo={nodeInfo}
|
||||
showSteps={false}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(LastRun)
|
@@ -0,0 +1,36 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { ClockPlay } from '@/app/components/base/icons/src/vender/line/time'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { RiPlayLine } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type Props = {
|
||||
canSingleRun: boolean
|
||||
onSingleRun: () => void
|
||||
}
|
||||
|
||||
const NoData: FC<Props> = ({
|
||||
canSingleRun,
|
||||
onSingleRun,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div className='flex h-0 grow flex-col items-center justify-center'>
|
||||
<ClockPlay className='h-8 w-8 text-text-quaternary' />
|
||||
<div className='system-xs-regular my-2 text-text-tertiary'>{t('workflow.debug.noData.description')}</div>
|
||||
{canSingleRun && (
|
||||
<Button
|
||||
className='flex'
|
||||
size='small'
|
||||
onClick={onSingleRun}
|
||||
>
|
||||
<RiPlayLine className='mr-1 h-3.5 w-3.5' />
|
||||
<div>{t('workflow.debug.noData.runThisNode')}</div>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(NoData)
|
@@ -0,0 +1,330 @@
|
||||
import useOneStepRun from '@/app/components/workflow/nodes/_base/hooks/use-one-step-run'
|
||||
import type { Params as OneStepRunParams } from '@/app/components/workflow/nodes/_base/hooks/use-one-step-run'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { TabType } from '../tab'
|
||||
import type { Props as FormProps } from '@/app/components/workflow/nodes/_base/components/before-run-form/form'
|
||||
import useStartSingleRunFormParams from '@/app/components/workflow/nodes/start/use-single-run-form-params'
|
||||
import useLLMSingleRunFormParams from '@/app/components/workflow/nodes/llm/use-single-run-form-params'
|
||||
import useKnowledgeRetrievalSingleRunFormParams from '@/app/components/workflow/nodes/knowledge-retrieval/use-single-run-form-params'
|
||||
import useCodeSingleRunFormParams from '@/app/components/workflow/nodes/code/use-single-run-form-params'
|
||||
import useTemplateTransformSingleRunFormParams from '@/app/components/workflow/nodes/template-transform/use-single-run-form-params'
|
||||
import useQuestionClassifierSingleRunFormParams from '@/app/components/workflow/nodes/question-classifier/use-single-run-form-params'
|
||||
import useParameterExtractorSingleRunFormParams from '@/app/components/workflow/nodes/parameter-extractor/use-single-run-form-params'
|
||||
import useHttpRequestSingleRunFormParams from '@/app/components/workflow/nodes/http/use-single-run-form-params'
|
||||
import useToolSingleRunFormParams from '@/app/components/workflow/nodes/tool/use-single-run-form-params'
|
||||
import useIterationSingleRunFormParams from '@/app/components/workflow/nodes/iteration/use-single-run-form-params'
|
||||
import useAgentSingleRunFormParams from '@/app/components/workflow/nodes/agent/use-single-run-form-params'
|
||||
import useDocExtractorSingleRunFormParams from '@/app/components/workflow/nodes/document-extractor/use-single-run-form-params'
|
||||
import useLoopSingleRunFormParams from '@/app/components/workflow/nodes/loop/use-single-run-form-params'
|
||||
import useIfElseSingleRunFormParams from '@/app/components/workflow/nodes/if-else/use-single-run-form-params'
|
||||
import useVariableAggregatorSingleRunFormParams from '@/app/components/workflow/nodes/variable-assigner/use-single-run-form-params'
|
||||
import useVariableAssignerSingleRunFormParams from '@/app/components/workflow/nodes/assigner/use-single-run-form-params'
|
||||
|
||||
import useToolGetDataForCheckMore from '@/app/components/workflow/nodes/tool/use-get-data-for-check-more'
|
||||
import { VALUE_SELECTOR_DELIMITER as DELIMITER } from '@/config'
|
||||
|
||||
// import
|
||||
import type { CommonNodeType, ValueSelector } from '@/app/components/workflow/types'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import {
|
||||
useNodesSyncDraft,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
import useInspectVarsCrud from '@/app/components/workflow/hooks/use-inspect-vars-crud'
|
||||
import { useInvalidLastRun } from '@/service/use-workflow'
|
||||
import { useStore, useWorkflowStore } from '@/app/components/workflow/store'
|
||||
|
||||
const singleRunFormParamsHooks: Record<BlockEnum, any> = {
|
||||
[BlockEnum.LLM]: useLLMSingleRunFormParams,
|
||||
[BlockEnum.KnowledgeRetrieval]: useKnowledgeRetrievalSingleRunFormParams,
|
||||
[BlockEnum.Code]: useCodeSingleRunFormParams,
|
||||
[BlockEnum.TemplateTransform]: useTemplateTransformSingleRunFormParams,
|
||||
[BlockEnum.QuestionClassifier]: useQuestionClassifierSingleRunFormParams,
|
||||
[BlockEnum.HttpRequest]: useHttpRequestSingleRunFormParams,
|
||||
[BlockEnum.Tool]: useToolSingleRunFormParams,
|
||||
[BlockEnum.ParameterExtractor]: useParameterExtractorSingleRunFormParams,
|
||||
[BlockEnum.Iteration]: useIterationSingleRunFormParams,
|
||||
[BlockEnum.Agent]: useAgentSingleRunFormParams,
|
||||
[BlockEnum.DocExtractor]: useDocExtractorSingleRunFormParams,
|
||||
[BlockEnum.Loop]: useLoopSingleRunFormParams,
|
||||
[BlockEnum.Start]: useStartSingleRunFormParams,
|
||||
[BlockEnum.IfElse]: useIfElseSingleRunFormParams,
|
||||
[BlockEnum.VariableAggregator]: useVariableAggregatorSingleRunFormParams,
|
||||
[BlockEnum.Assigner]: useVariableAssignerSingleRunFormParams,
|
||||
[BlockEnum.VariableAssigner]: undefined,
|
||||
[BlockEnum.End]: undefined,
|
||||
[BlockEnum.Answer]: undefined,
|
||||
[BlockEnum.ListFilter]: undefined,
|
||||
[BlockEnum.IterationStart]: undefined,
|
||||
[BlockEnum.LoopStart]: undefined,
|
||||
[BlockEnum.LoopEnd]: undefined,
|
||||
}
|
||||
|
||||
const useSingleRunFormParamsHooks = (nodeType: BlockEnum) => {
|
||||
return (params: any) => {
|
||||
return singleRunFormParamsHooks[nodeType]?.(params) || {}
|
||||
}
|
||||
}
|
||||
|
||||
const getDataForCheckMoreHooks: Record<BlockEnum, any> = {
|
||||
[BlockEnum.Tool]: useToolGetDataForCheckMore,
|
||||
[BlockEnum.LLM]: undefined,
|
||||
[BlockEnum.KnowledgeRetrieval]: undefined,
|
||||
[BlockEnum.Code]: undefined,
|
||||
[BlockEnum.TemplateTransform]: undefined,
|
||||
[BlockEnum.QuestionClassifier]: undefined,
|
||||
[BlockEnum.HttpRequest]: undefined,
|
||||
[BlockEnum.ParameterExtractor]: undefined,
|
||||
[BlockEnum.Iteration]: undefined,
|
||||
[BlockEnum.Agent]: undefined,
|
||||
[BlockEnum.DocExtractor]: undefined,
|
||||
[BlockEnum.Loop]: undefined,
|
||||
[BlockEnum.Start]: undefined,
|
||||
[BlockEnum.IfElse]: undefined,
|
||||
[BlockEnum.VariableAggregator]: undefined,
|
||||
[BlockEnum.End]: undefined,
|
||||
[BlockEnum.Answer]: undefined,
|
||||
[BlockEnum.VariableAssigner]: undefined,
|
||||
[BlockEnum.ListFilter]: undefined,
|
||||
[BlockEnum.IterationStart]: undefined,
|
||||
[BlockEnum.Assigner]: undefined,
|
||||
[BlockEnum.LoopStart]: undefined,
|
||||
[BlockEnum.LoopEnd]: undefined,
|
||||
}
|
||||
|
||||
const useGetDataForCheckMoreHooks = <T>(nodeType: BlockEnum) => {
|
||||
return (id: string, payload: CommonNodeType<T>) => {
|
||||
return getDataForCheckMoreHooks[nodeType]?.({ id, payload }) || {
|
||||
getData: () => {
|
||||
return {}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Params<T> = Omit<OneStepRunParams<T>, 'isRunAfterSingleRun'>
|
||||
const useLastRun = <T>({
|
||||
...oneStepRunParams
|
||||
}: Params<T>) => {
|
||||
const { conversationVars, systemVars, hasSetInspectVar } = useInspectVarsCrud()
|
||||
const blockType = oneStepRunParams.data.type
|
||||
const isStartNode = blockType === BlockEnum.Start
|
||||
const isIterationNode = blockType === BlockEnum.Iteration
|
||||
const isLoopNode = blockType === BlockEnum.Loop
|
||||
const isAggregatorNode = blockType === BlockEnum.VariableAggregator
|
||||
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
const {
|
||||
getData: getDataForCheckMore,
|
||||
} = useGetDataForCheckMoreHooks<T>(blockType)(oneStepRunParams.id, oneStepRunParams.data)
|
||||
const [isRunAfterSingleRun, setIsRunAfterSingleRun] = useState(false)
|
||||
|
||||
const {
|
||||
id,
|
||||
data,
|
||||
} = oneStepRunParams
|
||||
const oneStepRunRes = useOneStepRun({
|
||||
...oneStepRunParams,
|
||||
iteratorInputKey: blockType === BlockEnum.Iteration ? `${id}.input_selector` : '',
|
||||
moreDataForCheckValid: getDataForCheckMore(),
|
||||
isRunAfterSingleRun,
|
||||
})
|
||||
|
||||
const {
|
||||
appId,
|
||||
hideSingleRun,
|
||||
handleRun: doCallRunApi,
|
||||
getInputVars,
|
||||
toVarInputs,
|
||||
varSelectorsToVarInputs,
|
||||
runInputData,
|
||||
runInputDataRef,
|
||||
setRunInputData,
|
||||
showSingleRun,
|
||||
runResult,
|
||||
iterationRunResult,
|
||||
loopRunResult,
|
||||
setNodeRunning,
|
||||
checkValid,
|
||||
} = oneStepRunRes
|
||||
|
||||
const {
|
||||
nodeInfo,
|
||||
...singleRunParams
|
||||
} = useSingleRunFormParamsHooks(blockType)({
|
||||
id,
|
||||
payload: data,
|
||||
runInputData,
|
||||
runInputDataRef,
|
||||
getInputVars,
|
||||
setRunInputData,
|
||||
toVarInputs,
|
||||
varSelectorsToVarInputs,
|
||||
runResult,
|
||||
iterationRunResult,
|
||||
loopRunResult,
|
||||
})
|
||||
|
||||
const toSubmitData = useCallback((data: Record<string, any>) => {
|
||||
if(!isIterationNode && !isLoopNode)
|
||||
return data
|
||||
|
||||
const allVarObject = singleRunParams?.allVarObject || {}
|
||||
const formattedData: Record<string, any> = {}
|
||||
Object.keys(allVarObject).forEach((key) => {
|
||||
const [varSectorStr, nodeId] = key.split(DELIMITER)
|
||||
formattedData[`${nodeId}.${allVarObject[key].inSingleRunPassedKey}`] = data[varSectorStr]
|
||||
})
|
||||
if(isIterationNode) {
|
||||
const iteratorInputKey = `${id}.input_selector`
|
||||
formattedData[iteratorInputKey] = data[iteratorInputKey]
|
||||
}
|
||||
return formattedData
|
||||
}, [isIterationNode, isLoopNode, singleRunParams?.allVarObject, id])
|
||||
|
||||
const callRunApi = (data: Record<string, any>, cb?: () => void) => {
|
||||
handleSyncWorkflowDraft(true, true, {
|
||||
onSuccess() {
|
||||
doCallRunApi(toSubmitData(data))
|
||||
cb?.()
|
||||
},
|
||||
})
|
||||
}
|
||||
const workflowStore = useWorkflowStore()
|
||||
const { setInitShowLastRunTab } = workflowStore.getState()
|
||||
const initShowLastRunTab = useStore(s => s.initShowLastRunTab)
|
||||
const [tabType, setTabType] = useState<TabType>(initShowLastRunTab ? TabType.lastRun : TabType.settings)
|
||||
useEffect(() => {
|
||||
if(initShowLastRunTab)
|
||||
setTabType(TabType.lastRun)
|
||||
|
||||
setInitShowLastRunTab(false)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [initShowLastRunTab])
|
||||
const invalidLastRun = useInvalidLastRun(appId!, id)
|
||||
|
||||
const handleRunWithParams = async (data: Record<string, any>) => {
|
||||
const { isValid } = checkValid()
|
||||
if(!isValid)
|
||||
return
|
||||
setNodeRunning()
|
||||
setIsRunAfterSingleRun(true)
|
||||
setTabType(TabType.lastRun)
|
||||
callRunApi(data, () => {
|
||||
invalidLastRun()
|
||||
})
|
||||
hideSingleRun()
|
||||
}
|
||||
|
||||
const handleTabClicked = useCallback((type: TabType) => {
|
||||
setIsRunAfterSingleRun(false)
|
||||
setTabType(type)
|
||||
}, [])
|
||||
|
||||
const getExistVarValuesInForms = (forms: FormProps[]) => {
|
||||
if (!forms || forms.length === 0)
|
||||
return []
|
||||
|
||||
const valuesArr = forms.map((form) => {
|
||||
const values: Record<string, boolean> = {}
|
||||
form.inputs.forEach(({ variable, getVarValueFromDependent }) => {
|
||||
const isGetValueFromDependent = getVarValueFromDependent || !variable.includes('.')
|
||||
if(isGetValueFromDependent && !singleRunParams?.getDependentVar)
|
||||
return
|
||||
|
||||
const selector = isGetValueFromDependent ? (singleRunParams?.getDependentVar(variable) || []) : variable.slice(1, -1).split('.')
|
||||
if(!selector || selector.length === 0)
|
||||
return
|
||||
const [nodeId, varName] = selector.slice(0, 2)
|
||||
if(!isStartNode && nodeId === id) { // inner vars like loop vars
|
||||
values[variable] = true
|
||||
return
|
||||
}
|
||||
const inspectVarValue = hasSetInspectVar(nodeId, varName, systemVars, conversationVars) // also detect system var , env and conversation var
|
||||
if (inspectVarValue)
|
||||
values[variable] = true
|
||||
})
|
||||
return values
|
||||
})
|
||||
return valuesArr
|
||||
}
|
||||
|
||||
const isAllVarsHasValue = (vars?: ValueSelector[]) => {
|
||||
if(!vars || vars.length === 0)
|
||||
return true
|
||||
return vars.every((varItem) => {
|
||||
const [nodeId, varName] = varItem.slice(0, 2)
|
||||
const inspectVarValue = hasSetInspectVar(nodeId, varName, systemVars, conversationVars) // also detect system var , env and conversation var
|
||||
return inspectVarValue
|
||||
})
|
||||
}
|
||||
|
||||
const isSomeVarsHasValue = (vars?: ValueSelector[]) => {
|
||||
if(!vars || vars.length === 0)
|
||||
return true
|
||||
return vars.some((varItem) => {
|
||||
const [nodeId, varName] = varItem.slice(0, 2)
|
||||
const inspectVarValue = hasSetInspectVar(nodeId, varName, systemVars, conversationVars) // also detect system var , env and conversation var
|
||||
return inspectVarValue
|
||||
})
|
||||
}
|
||||
const getFilteredExistVarForms = (forms: FormProps[]) => {
|
||||
if (!forms || forms.length === 0)
|
||||
return []
|
||||
|
||||
const existVarValuesInForms = getExistVarValuesInForms(forms)
|
||||
|
||||
const res = forms.map((form, i) => {
|
||||
const existVarValuesInForm = existVarValuesInForms[i]
|
||||
const newForm = { ...form }
|
||||
const inputs = form.inputs.filter((input) => {
|
||||
return !(input.variable in existVarValuesInForm)
|
||||
})
|
||||
newForm.inputs = inputs
|
||||
return newForm
|
||||
}).filter(form => form.inputs.length > 0)
|
||||
return res
|
||||
}
|
||||
|
||||
const checkAggregatorVarsSet = (vars: ValueSelector[][]) => {
|
||||
if(!vars || vars.length === 0)
|
||||
return true
|
||||
// in each group, at last one set is ok
|
||||
return vars.every((varItem) => {
|
||||
return isSomeVarsHasValue(varItem)
|
||||
})
|
||||
}
|
||||
|
||||
const handleSingleRun = () => {
|
||||
const { isValid } = checkValid()
|
||||
if(!isValid)
|
||||
return
|
||||
const vars = singleRunParams?.getDependentVars?.()
|
||||
// no need to input params
|
||||
if (isAggregatorNode ? checkAggregatorVarsSet(vars) : isAllVarsHasValue(vars)) {
|
||||
callRunApi({}, async () => {
|
||||
setIsRunAfterSingleRun(true)
|
||||
setNodeRunning()
|
||||
invalidLastRun()
|
||||
setTabType(TabType.lastRun)
|
||||
})
|
||||
}
|
||||
else {
|
||||
showSingleRun()
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...oneStepRunRes,
|
||||
tabType,
|
||||
isRunAfterSingleRun,
|
||||
setTabType: handleTabClicked,
|
||||
singleRunParams,
|
||||
nodeInfo,
|
||||
setRunInputData,
|
||||
handleSingleRun,
|
||||
handleRunWithParams,
|
||||
getExistVarValuesInForms,
|
||||
getFilteredExistVarForms,
|
||||
}
|
||||
}
|
||||
|
||||
export default useLastRun
|
@@ -0,0 +1,34 @@
|
||||
'use client'
|
||||
import TabHeader from '@/app/components/base/tab-header'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export enum TabType {
|
||||
settings = 'settings',
|
||||
lastRun = 'lastRun',
|
||||
}
|
||||
|
||||
type Props = {
|
||||
value: TabType,
|
||||
onChange: (value: TabType) => void
|
||||
}
|
||||
|
||||
const Tab: FC<Props> = ({
|
||||
value,
|
||||
onChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<TabHeader
|
||||
items={[
|
||||
{ id: TabType.settings, name: t('workflow.debug.settingsTab').toLocaleUpperCase() },
|
||||
{ id: TabType.lastRun, name: t('workflow.debug.lastRunTab').toLocaleUpperCase() },
|
||||
]}
|
||||
itemClassName='ml-0'
|
||||
value={value}
|
||||
onChange={onChange as any}
|
||||
/>
|
||||
)
|
||||
}
|
||||
export default React.memo(Tab)
|
@@ -13,7 +13,7 @@ import type { CommonNodeType, InputVar, ValueSelector, Var, Variable } from '@/a
|
||||
import { BlockEnum, InputVarType, NodeRunningStatus, VarType } from '@/app/components/workflow/types'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { useStore, useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import { getIterationSingleNodeRunUrl, getLoopSingleNodeRunUrl, singleNodeRun } from '@/service/workflow'
|
||||
import { fetchNodeInspectVars, getIterationSingleNodeRunUrl, getLoopSingleNodeRunUrl, singleNodeRun } from '@/service/workflow'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import LLMDefault from '@/app/components/workflow/nodes/llm/default'
|
||||
import KnowledgeRetrievalDefault from '@/app/components/workflow/nodes/knowledge-retrieval/default'
|
||||
@@ -32,7 +32,7 @@ import LoopDefault from '@/app/components/workflow/nodes/loop/default'
|
||||
import { ssePost } from '@/service/base'
|
||||
import { noop } from 'lodash-es'
|
||||
import { getInputVars as doGetInputVars } from '@/app/components/base/prompt-editor/constants'
|
||||
import type { NodeTracing } from '@/types/workflow'
|
||||
import type { NodeRunResult, NodeTracing } from '@/types/workflow'
|
||||
const { checkValid: checkLLMValid } = LLMDefault
|
||||
const { checkValid: checkKnowledgeRetrievalValid } = KnowledgeRetrievalDefault
|
||||
const { checkValid: checkIfElseValid } = IfElseDefault
|
||||
@@ -47,7 +47,11 @@ const { checkValid: checkParameterExtractorValid } = ParameterExtractorDefault
|
||||
const { checkValid: checkIterationValid } = IterationDefault
|
||||
const { checkValid: checkDocumentExtractorValid } = DocumentExtractorDefault
|
||||
const { checkValid: checkLoopValid } = LoopDefault
|
||||
|
||||
import {
|
||||
useStoreApi,
|
||||
} from 'reactflow'
|
||||
import { useInvalidLastRun } from '@/service/use-workflow'
|
||||
import useInspectVarsCrud from '../../../hooks/use-inspect-vars-crud'
|
||||
// eslint-disable-next-line ts/no-unsafe-function-type
|
||||
const checkValidFns: Record<BlockEnum, Function> = {
|
||||
[BlockEnum.LLM]: checkLLMValid,
|
||||
@@ -66,13 +70,15 @@ const checkValidFns: Record<BlockEnum, Function> = {
|
||||
[BlockEnum.Loop]: checkLoopValid,
|
||||
} as any
|
||||
|
||||
type Params<T> = {
|
||||
export type Params<T> = {
|
||||
id: string
|
||||
data: CommonNodeType<T>
|
||||
defaultRunInputData: Record<string, any>
|
||||
moreDataForCheckValid?: any
|
||||
iteratorInputKey?: string
|
||||
loopInputKey?: string
|
||||
isRunAfterSingleRun: boolean
|
||||
isPaused: boolean
|
||||
}
|
||||
|
||||
const varTypeToInputVarType = (type: VarType, {
|
||||
@@ -105,6 +111,8 @@ const useOneStepRun = <T>({
|
||||
moreDataForCheckValid,
|
||||
iteratorInputKey,
|
||||
loopInputKey,
|
||||
isRunAfterSingleRun,
|
||||
isPaused,
|
||||
}: Params<T>) => {
|
||||
const { t } = useTranslation()
|
||||
const { getBeforeNodesInSameBranch, getBeforeNodesInSameBranchIncludeParent } = useWorkflow() as any
|
||||
@@ -112,6 +120,7 @@ const useOneStepRun = <T>({
|
||||
const isChatMode = useIsChatMode()
|
||||
const isIteration = data.type === BlockEnum.Iteration
|
||||
const isLoop = data.type === BlockEnum.Loop
|
||||
const isStartNode = data.type === BlockEnum.Start
|
||||
|
||||
const availableNodes = getBeforeNodesInSameBranch(id)
|
||||
const availableNodesIncludeParent = getBeforeNodesInSameBranchIncludeParent(id)
|
||||
@@ -143,6 +152,7 @@ const useOneStepRun = <T>({
|
||||
}
|
||||
|
||||
const checkValid = checkValidFns[data.type]
|
||||
|
||||
const appId = useAppStore.getState().appDetail?.id
|
||||
const [runInputData, setRunInputData] = useState<Record<string, any>>(defaultRunInputData || {})
|
||||
const runInputDataRef = useRef(runInputData)
|
||||
@@ -150,11 +160,82 @@ const useOneStepRun = <T>({
|
||||
runInputDataRef.current = data
|
||||
setRunInputData(data)
|
||||
}, [])
|
||||
const iterationTimes = iteratorInputKey ? runInputData[iteratorInputKey].length : 0
|
||||
const loopTimes = loopInputKey ? runInputData[loopInputKey].length : 0
|
||||
const [runResult, setRunResult] = useState<any>(null)
|
||||
const iterationTimes = iteratorInputKey ? runInputData[iteratorInputKey]?.length : 0
|
||||
const loopTimes = loopInputKey ? runInputData[loopInputKey]?.length : 0
|
||||
|
||||
const store = useStoreApi()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const {
|
||||
setShowSingleRunPanel,
|
||||
} = workflowStore.getState()
|
||||
const invalidLastRun = useInvalidLastRun(appId!, id)
|
||||
const [runResult, doSetRunResult] = useState<NodeRunResult | null>(null)
|
||||
const {
|
||||
appendNodeInspectVars,
|
||||
invalidateSysVarValues,
|
||||
invalidateConversationVarValues,
|
||||
} = useInspectVarsCrud()
|
||||
const runningStatus = data._singleRunningStatus || NodeRunningStatus.NotStart
|
||||
const isPausedRef = useRef(isPaused)
|
||||
useEffect(() => {
|
||||
isPausedRef.current = isPaused
|
||||
}, [isPaused])
|
||||
|
||||
const setRunResult = useCallback(async (data: NodeRunResult | null) => {
|
||||
const isPaused = isPausedRef.current
|
||||
|
||||
// The backend don't support pause the single run, so the frontend handle the pause state.
|
||||
if(isPaused)
|
||||
return
|
||||
|
||||
const canRunLastRun = !isRunAfterSingleRun || runningStatus === NodeRunningStatus.Succeeded
|
||||
if(!canRunLastRun) {
|
||||
doSetRunResult(data)
|
||||
return
|
||||
}
|
||||
|
||||
// run fail may also update the inspect vars when the node set the error default output.
|
||||
const vars = await fetchNodeInspectVars(appId!, id)
|
||||
const { getNodes } = store.getState()
|
||||
const nodes = getNodes()
|
||||
appendNodeInspectVars(id, vars, nodes)
|
||||
if(data?.status === NodeRunningStatus.Succeeded) {
|
||||
invalidLastRun()
|
||||
if(isStartNode)
|
||||
invalidateSysVarValues()
|
||||
invalidateConversationVarValues() // loop, iteration, variable assigner node can update the conversation variables, but to simple the logic(some nodes may also can update in the future), all nodes refresh.
|
||||
}
|
||||
}, [isRunAfterSingleRun, runningStatus, appId, id, store, appendNodeInspectVars, invalidLastRun, isStartNode, invalidateSysVarValues, invalidateConversationVarValues])
|
||||
|
||||
const { handleNodeDataUpdate }: { handleNodeDataUpdate: (data: any) => void } = useNodeDataUpdate()
|
||||
const setNodeRunning = () => {
|
||||
handleNodeDataUpdate({
|
||||
id,
|
||||
data: {
|
||||
...data,
|
||||
_singleRunningStatus: NodeRunningStatus.Running,
|
||||
},
|
||||
})
|
||||
}
|
||||
const checkValidWrap = () => {
|
||||
if(!checkValid)
|
||||
return { isValid: true, errorMessage: '' }
|
||||
const res = checkValid(data, t, moreDataForCheckValid)
|
||||
if(!res.isValid) {
|
||||
handleNodeDataUpdate({
|
||||
id,
|
||||
data: {
|
||||
...data,
|
||||
_isSingleRun: false,
|
||||
},
|
||||
})
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: res.errorMessage,
|
||||
})
|
||||
}
|
||||
return res
|
||||
}
|
||||
const [canShowSingleRun, setCanShowSingleRun] = useState(false)
|
||||
const isShowSingleRun = data._isSingleRun && canShowSingleRun
|
||||
const [iterationRunResult, setIterationRunResult] = useState<NodeTracing[]>([])
|
||||
@@ -167,29 +248,15 @@ const useOneStepRun = <T>({
|
||||
}
|
||||
|
||||
if (data._isSingleRun) {
|
||||
const { isValid, errorMessage } = checkValid(data, t, moreDataForCheckValid)
|
||||
const { isValid } = checkValidWrap()
|
||||
setCanShowSingleRun(isValid)
|
||||
if (!isValid) {
|
||||
handleNodeDataUpdate({
|
||||
id,
|
||||
data: {
|
||||
...data,
|
||||
_isSingleRun: false,
|
||||
},
|
||||
})
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: errorMessage,
|
||||
})
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [data._isSingleRun])
|
||||
|
||||
const workflowStore = useWorkflowStore()
|
||||
useEffect(() => {
|
||||
workflowStore.getState().setShowSingleRunPanel(!!isShowSingleRun)
|
||||
}, [isShowSingleRun, workflowStore])
|
||||
setShowSingleRunPanel(!!isShowSingleRun)
|
||||
}, [isShowSingleRun, setShowSingleRunPanel])
|
||||
|
||||
const hideSingleRun = () => {
|
||||
handleNodeDataUpdate({
|
||||
@@ -209,7 +276,6 @@ const useOneStepRun = <T>({
|
||||
},
|
||||
})
|
||||
}
|
||||
const runningStatus = data._singleRunningStatus || NodeRunningStatus.NotStart
|
||||
const isCompleted = runningStatus === NodeRunningStatus.Succeeded || runningStatus === NodeRunningStatus.Failed
|
||||
|
||||
const handleRun = async (submitData: Record<string, any>) => {
|
||||
@@ -217,13 +283,29 @@ const useOneStepRun = <T>({
|
||||
id,
|
||||
data: {
|
||||
...data,
|
||||
_isSingleRun: false,
|
||||
_singleRunningStatus: NodeRunningStatus.Running,
|
||||
},
|
||||
})
|
||||
let res: any
|
||||
let hasError = false
|
||||
try {
|
||||
if (!isIteration && !isLoop) {
|
||||
res = await singleNodeRun(appId!, id, { inputs: submitData }) as any
|
||||
const isStartNode = data.type === BlockEnum.Start
|
||||
const postData: Record<string, any> = {}
|
||||
if(isStartNode) {
|
||||
const { '#sys.query#': query, '#sys.files#': files, ...inputs } = submitData
|
||||
if(isChatMode)
|
||||
postData.conversation_id = ''
|
||||
|
||||
postData.inputs = inputs
|
||||
postData.query = query
|
||||
postData.files = files || []
|
||||
}
|
||||
else {
|
||||
postData.inputs = submitData
|
||||
}
|
||||
res = await singleNodeRun(appId!, id, postData) as any
|
||||
}
|
||||
else if (isIteration) {
|
||||
setIterationRunResult([])
|
||||
@@ -235,10 +317,13 @@ const useOneStepRun = <T>({
|
||||
{
|
||||
onWorkflowStarted: noop,
|
||||
onWorkflowFinished: (params) => {
|
||||
if(isPausedRef.current)
|
||||
return
|
||||
handleNodeDataUpdate({
|
||||
id,
|
||||
data: {
|
||||
...data,
|
||||
_isSingleRun: false,
|
||||
_singleRunningStatus: NodeRunningStatus.Succeeded,
|
||||
},
|
||||
})
|
||||
@@ -311,10 +396,13 @@ const useOneStepRun = <T>({
|
||||
setIterationRunResult(newIterationRunResult)
|
||||
},
|
||||
onError: () => {
|
||||
if(isPausedRef.current)
|
||||
return
|
||||
handleNodeDataUpdate({
|
||||
id,
|
||||
data: {
|
||||
...data,
|
||||
_isSingleRun: false,
|
||||
_singleRunningStatus: NodeRunningStatus.Failed,
|
||||
},
|
||||
})
|
||||
@@ -332,10 +420,13 @@ const useOneStepRun = <T>({
|
||||
{
|
||||
onWorkflowStarted: noop,
|
||||
onWorkflowFinished: (params) => {
|
||||
if(isPausedRef.current)
|
||||
return
|
||||
handleNodeDataUpdate({
|
||||
id,
|
||||
data: {
|
||||
...data,
|
||||
_isSingleRun: false,
|
||||
_singleRunningStatus: NodeRunningStatus.Succeeded,
|
||||
},
|
||||
})
|
||||
@@ -409,10 +500,13 @@ const useOneStepRun = <T>({
|
||||
setLoopRunResult(newLoopRunResult)
|
||||
},
|
||||
onError: () => {
|
||||
if(isPausedRef.current)
|
||||
return
|
||||
handleNodeDataUpdate({
|
||||
id,
|
||||
data: {
|
||||
...data,
|
||||
_isSingleRun: false,
|
||||
_singleRunningStatus: NodeRunningStatus.Failed,
|
||||
},
|
||||
})
|
||||
@@ -425,11 +519,16 @@ const useOneStepRun = <T>({
|
||||
}
|
||||
catch (e: any) {
|
||||
console.error(e)
|
||||
hasError = true
|
||||
invalidLastRun()
|
||||
if (!isIteration && !isLoop) {
|
||||
if(isPausedRef.current)
|
||||
return
|
||||
handleNodeDataUpdate({
|
||||
id,
|
||||
data: {
|
||||
...data,
|
||||
_isSingleRun: false,
|
||||
_singleRunningStatus: NodeRunningStatus.Failed,
|
||||
},
|
||||
})
|
||||
@@ -437,7 +536,7 @@ const useOneStepRun = <T>({
|
||||
}
|
||||
}
|
||||
finally {
|
||||
if (!isIteration && !isLoop) {
|
||||
if (!isPausedRef.current && !isIteration && !isLoop && res) {
|
||||
setRunResult({
|
||||
...res,
|
||||
total_tokens: res.execution_metadata?.total_tokens || 0,
|
||||
@@ -445,11 +544,17 @@ const useOneStepRun = <T>({
|
||||
})
|
||||
}
|
||||
}
|
||||
if (!isIteration && !isLoop) {
|
||||
if(isPausedRef.current)
|
||||
return
|
||||
|
||||
if (!isIteration && !isLoop && !hasError) {
|
||||
if(isPausedRef.current)
|
||||
return
|
||||
handleNodeDataUpdate({
|
||||
id,
|
||||
data: {
|
||||
...data,
|
||||
_isSingleRun: false,
|
||||
_singleRunningStatus: NodeRunningStatus.Succeeded,
|
||||
},
|
||||
})
|
||||
@@ -521,11 +626,19 @@ const useOneStepRun = <T>({
|
||||
return varInputs
|
||||
}
|
||||
|
||||
const varSelectorsToVarInputs = (valueSelectors: ValueSelector[] | string[]): InputVar[] => {
|
||||
return valueSelectors.filter(item => !!item).map((item) => {
|
||||
return getInputVars([`{{#${typeof item === 'string' ? item : item.join('.')}#}}`])[0]
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
appId,
|
||||
isShowSingleRun,
|
||||
hideSingleRun,
|
||||
showSingleRun,
|
||||
toVarInputs,
|
||||
varSelectorsToVarInputs,
|
||||
getInputVars,
|
||||
runningStatus,
|
||||
isCompleted,
|
||||
@@ -537,6 +650,8 @@ const useOneStepRun = <T>({
|
||||
runResult,
|
||||
iterationRunResult,
|
||||
loopRunResult,
|
||||
setNodeRunning,
|
||||
checkValid: checkValidWrap,
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import produce from 'immer'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { useBoolean, useDebounceFn } from 'ahooks'
|
||||
import type {
|
||||
CodeNodeType,
|
||||
OutputVar,
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
} from '@/app/components/workflow/hooks'
|
||||
import { ErrorHandleTypeEnum } from '@/app/components/workflow/nodes/_base/components/error-handle/types'
|
||||
import { getDefaultValue } from '@/app/components/workflow/nodes/_base/components/error-handle/utils'
|
||||
import useInspectVarsCrud from '../../../hooks/use-inspect-vars-crud'
|
||||
|
||||
type Params<T> = {
|
||||
id: string
|
||||
@@ -34,8 +35,27 @@ function useOutputVarList<T>({
|
||||
outputKeyOrders = [],
|
||||
onOutputKeyOrdersChange,
|
||||
}: Params<T>) {
|
||||
const {
|
||||
renameInspectVarName,
|
||||
deleteInspectVar,
|
||||
nodesWithInspectVars,
|
||||
} = useInspectVarsCrud()
|
||||
|
||||
const { handleOutVarRenameChange, isVarUsedInNodes, removeUsedVarInNodes } = useWorkflow()
|
||||
|
||||
// record the first old name value
|
||||
const oldNameRecord = useRef<Record<string, string>>({})
|
||||
|
||||
const {
|
||||
run: renameInspectNameWithDebounce,
|
||||
} = useDebounceFn(
|
||||
(id: string, newName: string) => {
|
||||
const oldName = oldNameRecord.current[id]
|
||||
renameInspectVarName(id, oldName, newName)
|
||||
delete oldNameRecord.current[id]
|
||||
},
|
||||
{ wait: 500 },
|
||||
)
|
||||
const handleVarsChange = useCallback((newVars: OutputVar, changedIndex?: number, newKey?: string) => {
|
||||
const newInputs = produce(inputs, (draft: any) => {
|
||||
draft[varKey] = newVars
|
||||
@@ -52,9 +72,20 @@ function useOutputVarList<T>({
|
||||
onOutputKeyOrdersChange(newOutputKeyOrders)
|
||||
}
|
||||
|
||||
if (newKey)
|
||||
if (newKey) {
|
||||
handleOutVarRenameChange(id, [id, outputKeyOrders[changedIndex!]], [id, newKey])
|
||||
}, [inputs, setInputs, handleOutVarRenameChange, id, outputKeyOrders, varKey, onOutputKeyOrdersChange])
|
||||
if(!(id in oldNameRecord.current))
|
||||
oldNameRecord.current[id] = outputKeyOrders[changedIndex!]
|
||||
renameInspectNameWithDebounce(id, newKey)
|
||||
}
|
||||
else if (changedIndex === undefined) {
|
||||
const varId = nodesWithInspectVars.find(node => node.nodeId === id)?.vars.find((varItem) => {
|
||||
return varItem.name === Object.keys(newVars)[0]
|
||||
})?.id
|
||||
if(varId)
|
||||
deleteInspectVar(id, varId)
|
||||
}
|
||||
}, [inputs, setInputs, varKey, outputKeyOrders, onOutputKeyOrdersChange, handleOutVarRenameChange, id, renameInspectNameWithDebounce, nodesWithInspectVars, deleteInspectVar])
|
||||
|
||||
const generateNewKey = useCallback(() => {
|
||||
let keyIndex = Object.keys((inputs as any)[varKey]).length + 1
|
||||
@@ -86,9 +117,14 @@ function useOutputVarList<T>({
|
||||
}] = useBoolean(false)
|
||||
const [removedVar, setRemovedVar] = useState<ValueSelector>([])
|
||||
const removeVarInNode = useCallback(() => {
|
||||
const varId = nodesWithInspectVars.find(node => node.nodeId === id)?.vars.find((varItem) => {
|
||||
return varItem.name === removedVar[1]
|
||||
})?.id
|
||||
if(varId)
|
||||
deleteInspectVar(id, varId)
|
||||
removeUsedVarInNodes(removedVar)
|
||||
hideRemoveVarConfirm()
|
||||
}, [hideRemoveVarConfirm, removeUsedVarInNodes, removedVar])
|
||||
}, [deleteInspectVar, hideRemoveVarConfirm, id, nodesWithInspectVars, removeUsedVarInNodes, removedVar])
|
||||
const handleRemoveVariable = useCallback((index: number) => {
|
||||
const key = outputKeyOrders[index]
|
||||
|
||||
@@ -106,7 +142,12 @@ function useOutputVarList<T>({
|
||||
})
|
||||
setInputs(newInputs)
|
||||
onOutputKeyOrdersChange(outputKeyOrders.filter((_, i) => i !== index))
|
||||
}, [outputKeyOrders, isVarUsedInNodes, id, inputs, setInputs, onOutputKeyOrdersChange, showRemoveVarConfirm, varKey])
|
||||
const varId = nodesWithInspectVars.find(node => node.nodeId === id)?.vars.find((varItem) => {
|
||||
return varItem.name === key
|
||||
})?.id
|
||||
if(varId)
|
||||
deleteInspectVar(id, varId)
|
||||
}, [outputKeyOrders, isVarUsedInNodes, id, inputs, setInputs, onOutputKeyOrdersChange, nodesWithInspectVars, deleteInspectVar, showRemoveVarConfirm, varKey])
|
||||
|
||||
return {
|
||||
handleVarsChange,
|
||||
|
@@ -44,6 +44,7 @@ import AddVariablePopupWithPosition from './components/add-variable-popup-with-p
|
||||
import cn from '@/utils/classnames'
|
||||
import BlockIcon from '@/app/components/workflow/block-icon'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import useInspectVarsCrud from '../../hooks/use-inspect-vars-crud'
|
||||
|
||||
type BaseNodeProps = {
|
||||
children: ReactElement
|
||||
@@ -89,6 +90,9 @@ const BaseNode: FC<BaseNodeProps> = ({
|
||||
}
|
||||
}, [data.isInLoop, data.selected, id, handleNodeLoopChildSizeChange])
|
||||
|
||||
const { hasNodeInspectVars } = useInspectVarsCrud()
|
||||
const isLoading = data._runningStatus === NodeRunningStatus.Running || data._singleRunningStatus === NodeRunningStatus.Running
|
||||
const hasVarValue = hasNodeInspectVars(id)
|
||||
const showSelectedBorder = data.selected || data._isBundled || data._isEntering
|
||||
const {
|
||||
showRunningBorder,
|
||||
@@ -98,11 +102,11 @@ const BaseNode: FC<BaseNodeProps> = ({
|
||||
} = useMemo(() => {
|
||||
return {
|
||||
showRunningBorder: data._runningStatus === NodeRunningStatus.Running && !showSelectedBorder,
|
||||
showSuccessBorder: data._runningStatus === NodeRunningStatus.Succeeded && !showSelectedBorder,
|
||||
showSuccessBorder: (data._runningStatus === NodeRunningStatus.Succeeded || hasVarValue) && !showSelectedBorder,
|
||||
showFailedBorder: data._runningStatus === NodeRunningStatus.Failed && !showSelectedBorder,
|
||||
showExceptionBorder: data._runningStatus === NodeRunningStatus.Exception && !showSelectedBorder,
|
||||
}
|
||||
}, [data._runningStatus, showSelectedBorder])
|
||||
}, [data._runningStatus, hasVarValue, showSelectedBorder])
|
||||
|
||||
const LoopIndex = useMemo(() => {
|
||||
let text = ''
|
||||
@@ -260,12 +264,12 @@ const BaseNode: FC<BaseNodeProps> = ({
|
||||
data.type === BlockEnum.Loop && data._loopIndex && LoopIndex
|
||||
}
|
||||
{
|
||||
(data._runningStatus === NodeRunningStatus.Running || data._singleRunningStatus === NodeRunningStatus.Running) && (
|
||||
isLoading && (
|
||||
<RiLoader2Line className='h-3.5 w-3.5 animate-spin text-text-accent' />
|
||||
)
|
||||
}
|
||||
{
|
||||
data._runningStatus === NodeRunningStatus.Succeeded && (
|
||||
(!isLoading && (data._runningStatus === NodeRunningStatus.Succeeded || hasVarValue)) && (
|
||||
<RiCheckboxCircleFill className='h-3.5 w-3.5 text-text-success' />
|
||||
)
|
||||
}
|
||||
|
@@ -1,214 +0,0 @@
|
||||
import type {
|
||||
FC,
|
||||
ReactNode,
|
||||
} from 'react'
|
||||
import {
|
||||
cloneElement,
|
||||
memo,
|
||||
useCallback,
|
||||
} from 'react'
|
||||
import {
|
||||
RiCloseLine,
|
||||
RiPlayLargeLine,
|
||||
} from '@remixicon/react'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import NextStep from './components/next-step'
|
||||
import PanelOperator from './components/panel-operator'
|
||||
import HelpLink from './components/help-link'
|
||||
import NodePosition from './components/node-position'
|
||||
import {
|
||||
DescriptionInput,
|
||||
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,
|
||||
useNodeDataUpdate,
|
||||
useNodesInteractions,
|
||||
useNodesReadOnly,
|
||||
useNodesSyncDraft,
|
||||
useToolIcon,
|
||||
useWorkflow,
|
||||
useWorkflowHistory,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
import {
|
||||
canRunBySingle,
|
||||
hasErrorHandleNode,
|
||||
hasRetryNode,
|
||||
} from '@/app/components/workflow/utils'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import type { Node } from '@/app/components/workflow/types'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
|
||||
type BasePanelProps = {
|
||||
children: ReactNode
|
||||
} & Node
|
||||
|
||||
const BasePanel: FC<BasePanelProps> = ({
|
||||
id,
|
||||
data,
|
||||
children,
|
||||
position,
|
||||
width,
|
||||
height,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { showMessageLogModal } = useAppStore(useShallow(state => ({
|
||||
showMessageLogModal: state.showMessageLogModal,
|
||||
})))
|
||||
const showSingleRunPanel = useStore(s => s.showSingleRunPanel)
|
||||
const panelWidth = localStorage.getItem('workflow-node-panel-width') ? Number.parseFloat(localStorage.getItem('workflow-node-panel-width')!) : 420
|
||||
const {
|
||||
setPanelWidth,
|
||||
} = useWorkflow()
|
||||
const { handleNodeSelect } = useNodesInteractions()
|
||||
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
const { nodesReadOnly } = useNodesReadOnly()
|
||||
const { availableNextBlocks } = useAvailableBlocks(data.type, data.isInIteration, data.isInLoop)
|
||||
const toolIcon = useToolIcon(data)
|
||||
|
||||
const handleResize = useCallback((width: number) => {
|
||||
setPanelWidth(width)
|
||||
}, [setPanelWidth])
|
||||
|
||||
const {
|
||||
triggerRef,
|
||||
containerRef,
|
||||
} = useResizePanel({
|
||||
direction: 'horizontal',
|
||||
triggerDirection: 'left',
|
||||
minWidth: 420,
|
||||
maxWidth: 720,
|
||||
onResize: handleResize,
|
||||
})
|
||||
|
||||
const { saveStateToHistory } = useWorkflowHistory()
|
||||
|
||||
const {
|
||||
handleNodeDataUpdate,
|
||||
handleNodeDataUpdateWithSyncDraft,
|
||||
} = useNodeDataUpdate()
|
||||
|
||||
const handleTitleBlur = useCallback((title: string) => {
|
||||
handleNodeDataUpdateWithSyncDraft({ id, data: { title } })
|
||||
saveStateToHistory(WorkflowHistoryEvent.NodeTitleChange)
|
||||
}, [handleNodeDataUpdateWithSyncDraft, id, saveStateToHistory])
|
||||
const handleDescriptionChange = useCallback((desc: string) => {
|
||||
handleNodeDataUpdateWithSyncDraft({ id, data: { desc } })
|
||||
saveStateToHistory(WorkflowHistoryEvent.NodeDescriptionChange)
|
||||
}, [handleNodeDataUpdateWithSyncDraft, id, saveStateToHistory])
|
||||
|
||||
return (
|
||||
<div className={cn(
|
||||
'relative mr-2 h-full',
|
||||
showMessageLogModal && '!absolute -top-[5px] right-[416px] z-0 !mr-0 w-[384px] overflow-hidden rounded-2xl border-[0.5px] border-components-panel-border shadow-lg transition-all',
|
||||
)}>
|
||||
<div
|
||||
ref={triggerRef}
|
||||
className='absolute -left-2 top-1/2 h-6 w-3 -translate-y-1/2 cursor-col-resize resize-x'>
|
||||
<div className='h-6 w-1 rounded-sm bg-divider-regular'></div>
|
||||
</div>
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={cn('h-full rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg', showSingleRunPanel ? 'overflow-hidden' : 'overflow-y-auto')}
|
||||
style={{
|
||||
width: `${panelWidth}px`,
|
||||
}}
|
||||
>
|
||||
<div className='sticky top-0 z-10 border-b-[0.5px] border-divider-regular bg-components-panel-bg'>
|
||||
<div className='flex items-center px-4 pb-1 pt-4'>
|
||||
<BlockIcon
|
||||
className='mr-1 shrink-0'
|
||||
type={data.type}
|
||||
toolIcon={toolIcon}
|
||||
size='md'
|
||||
/>
|
||||
<TitleInput
|
||||
value={data.title || ''}
|
||||
onBlur={handleTitleBlur}
|
||||
/>
|
||||
<div className='flex shrink-0 items-center text-text-tertiary'>
|
||||
{
|
||||
canRunBySingle(data.type) && !nodesReadOnly && (
|
||||
<Tooltip
|
||||
popupContent={t('workflow.panel.runThisStep')}
|
||||
popupClassName='mr-1'
|
||||
>
|
||||
<div
|
||||
className='mr-1 flex h-6 w-6 cursor-pointer items-center justify-center rounded-md hover:bg-state-base-hover'
|
||||
onClick={() => {
|
||||
handleNodeDataUpdate({ id, data: { _isSingleRun: true } })
|
||||
handleSyncWorkflowDraft(true)
|
||||
}}
|
||||
>
|
||||
<RiPlayLargeLine className='h-4 w-4 text-text-tertiary' />
|
||||
</div>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
<NodePosition nodePosition={position} nodeWidth={width} nodeHeight={height}></NodePosition>
|
||||
<HelpLink nodeType={data.type} />
|
||||
<PanelOperator id={id} data={data} showHelpLink={false} />
|
||||
<div className='mx-3 h-3.5 w-[1px] bg-divider-regular' />
|
||||
<div
|
||||
className='flex h-6 w-6 cursor-pointer items-center justify-center'
|
||||
onClick={() => handleNodeSelect(id, true)}
|
||||
>
|
||||
<RiCloseLine className='h-4 w-4 text-text-tertiary' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='p-2'>
|
||||
<DescriptionInput
|
||||
value={data.desc || ''}
|
||||
onChange={handleDescriptionChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{cloneElement(children as any, { id, data })}
|
||||
</div>
|
||||
<Split />
|
||||
{
|
||||
hasRetryNode(data.type) && (
|
||||
<RetryOnPanel
|
||||
id={id}
|
||||
data={data}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
hasErrorHandleNode(data.type) && (
|
||||
<ErrorHandleOnPanel
|
||||
id={id}
|
||||
data={data}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
!!availableNextBlocks.length && (
|
||||
<div className='border-t-[0.5px] border-divider-regular p-4'>
|
||||
<div className='system-sm-semibold-uppercase mb-1 flex items-center text-text-secondary'>
|
||||
{t('workflow.panel.nextStep').toLocaleUpperCase()}
|
||||
</div>
|
||||
<div className='system-xs-regular mb-2 text-text-tertiary'>
|
||||
{t('workflow.panel.addNextStep')}
|
||||
</div>
|
||||
<NextStep selectedNode={{ id, data } as Node} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(BasePanel)
|
Reference in New Issue
Block a user