feat(web): test run of snippet

This commit is contained in:
JzoNg
2026-03-29 20:55:11 +08:00
parent 2f88da4a6d
commit 768b3eb6f9
8 changed files with 875 additions and 0 deletions

View File

@@ -15,6 +15,13 @@ const mockToggleInputPanel = vi.fn()
const mockTogglePublishMenu = vi.fn()
const mockPublishSnippetMutateAsync = vi.fn()
const mockFetchInspectVars = vi.fn()
const mockHandleBackupDraft = vi.fn()
const mockHandleLoadBackupDraft = vi.fn()
const mockHandleRestoreFromPublishedWorkflow = vi.fn()
const mockHandleRun = vi.fn()
const mockHandleStartWorkflowRun = vi.fn()
const mockHandleStopRun = vi.fn()
const mockHandleWorkflowStartRunInWorkflow = vi.fn()
const mockInspectVarsCrud = {
hasNodeInspectVars: vi.fn(),
hasSetInspectVar: vi.fn(),
@@ -111,6 +118,23 @@ vi.mock('@/app/components/snippets/hooks/use-snippet-refresh-draft', () => ({
}),
}))
vi.mock('@/app/components/snippets/hooks/use-snippet-run', () => ({
useSnippetRun: () => ({
handleBackupDraft: mockHandleBackupDraft,
handleLoadBackupDraft: mockHandleLoadBackupDraft,
handleRestoreFromPublishedWorkflow: mockHandleRestoreFromPublishedWorkflow,
handleRun: mockHandleRun,
handleStopRun: mockHandleStopRun,
}),
}))
vi.mock('@/app/components/snippets/hooks/use-snippet-start-run', () => ({
useSnippetStartRun: () => ({
handleStartWorkflowRun: mockHandleStartWorkflowRun,
handleWorkflowStartRunInWorkflow: mockHandleWorkflowStartRunInWorkflow,
}),
}))
vi.mock('@/app/components/app-sidebar', () => ({
default: ({
renderHeader,
@@ -304,4 +328,18 @@ describe('SnippetMain', () => {
expect(capturedHooksStore?.invalidateConversationVarValues).toBe(mockInspectVarsCrud.invalidateConversationVarValues)
})
})
describe('Run Hooks', () => {
it('should pass snippet run handlers to WorkflowWithInnerContext', () => {
renderSnippetMain()
expect(capturedHooksStore?.handleBackupDraft).toBe(mockHandleBackupDraft)
expect(capturedHooksStore?.handleLoadBackupDraft).toBe(mockHandleLoadBackupDraft)
expect(capturedHooksStore?.handleRestoreFromPublishedWorkflow).toBe(mockHandleRestoreFromPublishedWorkflow)
expect(capturedHooksStore?.handleRun).toBe(mockHandleRun)
expect(capturedHooksStore?.handleStopRun).toBe(mockHandleStopRun)
expect(capturedHooksStore?.handleStartWorkflowRun).toBe(mockHandleStartWorkflowRun)
expect(capturedHooksStore?.handleWorkflowStartRunInWorkflow).toBe(mockHandleWorkflowStartRunInWorkflow)
})
})
})

View File

@@ -45,6 +45,7 @@ describe('SnippetWorkflowPanel', () => {
expect(capturedPanelProps?.versionHistoryPanelProps?.restoreVersionUrl('version-1')).toBe('/snippets/snippet-1/workflows/version-1/restore')
expect(capturedPanelProps?.versionHistoryPanelProps?.updateVersionUrl?.('version-1')).toBe('/snippets/snippet-1/workflows/version-1')
expect(capturedPanelProps?.versionHistoryPanelProps?.latestVersionId).toBe('')
expect(capturedPanelProps?.components?.right).toBeTruthy()
})
})
})

View File

@@ -28,6 +28,8 @@ import { useConfigsMap } from '../hooks/use-configs-map'
import { useInspectVarsCrud } from '../hooks/use-inspect-vars-crud'
import { useNodesSyncDraft } from '../hooks/use-nodes-sync-draft'
import { useSnippetRefreshDraft } from '../hooks/use-snippet-refresh-draft'
import { useSnippetRun } from '../hooks/use-snippet-run'
import { useSnippetStartRun } from '../hooks/use-snippet-start-run'
import { useSnippetDetailStore } from '../store'
import { useSnippetInputFieldActions } from './hooks/use-snippet-input-field-actions'
import { useSnippetPublish } from './hooks/use-snippet-publish'
@@ -66,6 +68,13 @@ const SnippetMain = ({
syncWorkflowDraftWhenPageClose,
} = useNodesSyncDraft(snippetId)
const { handleRefreshWorkflowDraft } = useSnippetRefreshDraft(snippetId)
const {
handleBackupDraft,
handleLoadBackupDraft,
handleRestoreFromPublishedWorkflow,
handleRun,
handleStopRun,
} = useSnippetRun(snippetId)
const configsMap = useConfigsMap(snippetId)
const { fetchInspectVars } = useSetWorkflowVarsWithValue({
...configsMap,
@@ -132,6 +141,13 @@ const SnippetMain = ({
snippetId,
section,
})
const {
handleStartWorkflowRun,
handleWorkflowStartRunInWorkflow,
} = useSnippetStartRun({
handleRun,
inputFields: fields,
})
useEffect(() => {
reset()
@@ -148,6 +164,13 @@ const SnippetMain = ({
doSyncWorkflowDraft,
syncWorkflowDraftWhenPageClose,
handleRefreshWorkflowDraft,
handleBackupDraft,
handleLoadBackupDraft,
handleRestoreFromPublishedWorkflow,
handleRun,
handleStopRun,
handleStartWorkflowRun,
handleWorkflowStartRunInWorkflow,
availableNodesMetaData,
fetchInspectVars,
hasNodeInspectVars,
@@ -177,7 +200,14 @@ const SnippetMain = ({
editInspectVarValue,
fetchInspectVarValue,
fetchInspectVars,
handleBackupDraft,
handleRefreshWorkflowDraft,
handleLoadBackupDraft,
handleRestoreFromPublishedWorkflow,
handleRun,
handleStartWorkflowRun,
handleStopRun,
handleWorkflowStartRunInWorkflow,
hasNodeInspectVars,
hasSetInspectVar,
invalidateConversationVarValues,

View File

@@ -0,0 +1,293 @@
'use client'
import type { InputForm } from '@/app/components/base/chat/chat/type'
import type { InputVar as WorkflowInputVar } from '@/app/components/workflow/types'
import type { SnippetInputField } from '@/models/snippet'
import copy from 'copy-to-clipboard'
import {
memo,
useCallback,
useEffect,
useMemo,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import { useCheckInputsForms } from '@/app/components/base/chat/chat/check-input-forms-hooks'
import { getProcessedInputs } from '@/app/components/base/chat/chat/utils'
import Loading from '@/app/components/base/loading'
import { toast } from '@/app/components/base/ui/toast'
import {
useWorkflowInteractions,
useWorkflowRun,
} from '@/app/components/workflow/hooks'
import FormItem from '@/app/components/workflow/nodes/_base/components/before-run-form/form-item'
import ResultPanel from '@/app/components/workflow/run/result-panel'
import ResultText from '@/app/components/workflow/run/result-text'
import TracingPanel from '@/app/components/workflow/run/tracing-panel'
import { useStore } from '@/app/components/workflow/store'
import {
InputVarType,
WorkflowRunningStatus,
} from '@/app/components/workflow/types'
import { formatWorkflowRunIdentifier } from '@/app/components/workflow/utils'
import { PipelineInputVarType } from '@/models/pipeline'
type SnippetRunPanelProps = {
fields: SnippetInputField[]
}
type SnippetRunField = WorkflowInputVar & InputForm
const PIPELINE_TO_WORKFLOW_INPUT_VAR_TYPE: Record<PipelineInputVarType, InputVarType> = {
[PipelineInputVarType.textInput]: InputVarType.textInput,
[PipelineInputVarType.paragraph]: InputVarType.paragraph,
[PipelineInputVarType.select]: InputVarType.select,
[PipelineInputVarType.number]: InputVarType.number,
[PipelineInputVarType.singleFile]: InputVarType.singleFile,
[PipelineInputVarType.multiFiles]: InputVarType.multiFiles,
[PipelineInputVarType.checkbox]: InputVarType.checkbox,
}
const buildPreviewFields = (fields: SnippetInputField[]): SnippetRunField[] => {
return fields.map(field => ({
type: PIPELINE_TO_WORKFLOW_INPUT_VAR_TYPE[field.type],
label: field.label,
variable: field.variable,
max_length: field.max_length,
default: field.default_value,
required: field.required,
options: field.options,
placeholder: field.placeholder,
unit: field.unit,
hide: false,
allowed_file_upload_methods: field.allowed_file_upload_methods,
allowed_file_types: field.allowed_file_types,
allowed_file_extensions: field.allowed_file_extensions,
}))
}
const buildInitialInputs = (fields: SnippetRunField[]) => {
return fields.reduce<Record<string, unknown>>((acc, field) => {
if (field.default !== undefined)
acc[field.variable] = field.default
return acc
}, {})
}
const SnippetRunPanel = ({
fields,
}: SnippetRunPanelProps) => {
const { t } = useTranslation()
const { handleCancelDebugAndPreviewPanel } = useWorkflowInteractions()
const { handleRun } = useWorkflowRun()
const { checkInputsForm } = useCheckInputsForms()
const workflowRunningData = useStore(s => s.workflowRunningData)
const showInputsPanel = useStore(s => s.showInputsPanel)
const workflowCanvasWidth = useStore(s => s.workflowCanvasWidth)
const panelWidth = useStore(s => s.previewPanelWidth)
const setPreviewPanelWidth = useStore(s => s.setPreviewPanelWidth)
const previewFields = useMemo(() => buildPreviewFields(fields), [fields])
const initialInputs = useMemo(() => buildInitialInputs(previewFields), [previewFields])
const [inputOverrides, setInputOverrides] = useState<Record<string, unknown> | null>(null)
const [selectedTab, setSelectedTab] = useState<string | null>(null)
const [isResizing, setIsResizing] = useState(false)
const inputs = inputOverrides ?? initialInputs
const hasInputTab = showInputsPanel && previewFields.length > 0
const defaultTab = hasInputTab ? 'INPUT' : 'RESULT'
const shouldShowDetailByDefault = !!workflowRunningData
&& (workflowRunningData.result.status === WorkflowRunningStatus.Succeeded || workflowRunningData.result.status === WorkflowRunningStatus.Failed)
&& !workflowRunningData.resultText
&& !workflowRunningData.result.files?.length
const currentTab = selectedTab ?? (shouldShowDetailByDefault ? 'DETAIL' : defaultTab)
const handleValueChange = useCallback((variable: string, value: unknown) => {
setInputOverrides(prev => ({
...(prev ?? initialInputs),
[variable]: value,
}))
}, [initialInputs])
const handleSubmit = useCallback(() => {
if (!checkInputsForm(inputs, previewFields))
return
setSelectedTab('RESULT')
handleRun({
inputs: getProcessedInputs(inputs, previewFields),
})
}, [checkInputsForm, handleRun, inputs, previewFields])
const startResizing = useCallback((e: React.MouseEvent) => {
e.preventDefault()
setIsResizing(true)
}, [])
const stopResizing = useCallback(() => {
setIsResizing(false)
}, [])
const resize = useCallback((e: MouseEvent) => {
if (!isResizing)
return
const newWidth = window.innerWidth - e.clientX
const reservedCanvasWidth = 400
const maxAllowed = workflowCanvasWidth ? (workflowCanvasWidth - reservedCanvasWidth) : 1024
if (newWidth >= 400 && newWidth <= maxAllowed)
setPreviewPanelWidth(newWidth)
}, [isResizing, setPreviewPanelWidth, workflowCanvasWidth])
useEffect(() => {
window.addEventListener('mousemove', resize)
window.addEventListener('mouseup', stopResizing)
return () => {
window.removeEventListener('mousemove', resize)
window.removeEventListener('mouseup', stopResizing)
}
}, [resize, stopResizing])
return (
<div
className="relative flex h-full flex-col rounded-l-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-xl"
style={{ width: `${panelWidth}px` }}
>
<div
className="absolute bottom-0 left-[3px] top-1/2 z-50 h-6 w-[3px] cursor-col-resize rounded bg-gray-300"
onMouseDown={startResizing}
/>
<div className="flex items-center justify-between p-4 pb-1 text-base font-semibold text-text-primary">
{`Test Run${formatWorkflowRunIdentifier(workflowRunningData?.result.finished_at, workflowRunningData?.result.status)}`}
<div className="cursor-pointer p-1" onClick={handleCancelDebugAndPreviewPanel}>
<span className="i-ri-close-line h-4 w-4 text-text-tertiary" />
</div>
</div>
<div className="relative flex grow flex-col">
<div className="flex shrink-0 items-center border-b-[0.5px] border-divider-subtle px-4">
{hasInputTab && (
<div
className={`mr-6 cursor-pointer border-b-2 py-3 text-[13px] font-semibold leading-[18px] ${currentTab === 'INPUT' ? '!border-[rgb(21,94,239)] text-text-secondary' : 'border-transparent text-text-tertiary'}`}
onClick={() => setSelectedTab('INPUT')}
>
{t('input', { ns: 'runLog' })}
</div>
)}
<div
className={`mr-6 cursor-pointer border-b-2 py-3 text-[13px] font-semibold leading-[18px] ${currentTab === 'RESULT' ? '!border-[rgb(21,94,239)] text-text-secondary' : 'border-transparent text-text-tertiary'} ${!workflowRunningData ? '!cursor-not-allowed opacity-30' : ''}`}
onClick={() => workflowRunningData && setSelectedTab('RESULT')}
>
{t('result', { ns: 'runLog' })}
</div>
<div
className={`mr-6 cursor-pointer border-b-2 py-3 text-[13px] font-semibold leading-[18px] ${currentTab === 'DETAIL' ? '!border-[rgb(21,94,239)] text-text-secondary' : 'border-transparent text-text-tertiary'} ${!workflowRunningData ? '!cursor-not-allowed opacity-30' : ''}`}
onClick={() => workflowRunningData && setSelectedTab('DETAIL')}
>
{t('detail', { ns: 'runLog' })}
</div>
<div
className={`mr-6 cursor-pointer border-b-2 py-3 text-[13px] font-semibold leading-[18px] ${currentTab === 'TRACING' ? '!border-[rgb(21,94,239)] text-text-secondary' : 'border-transparent text-text-tertiary'} ${!workflowRunningData ? '!cursor-not-allowed opacity-30' : ''}`}
onClick={() => workflowRunningData && setSelectedTab('TRACING')}
>
{t('tracing', { ns: 'runLog' })}
</div>
</div>
<div className={`h-0 grow overflow-y-auto rounded-b-2xl ${(currentTab === 'RESULT' || currentTab === 'TRACING') ? '!bg-background-section-burn' : 'bg-components-panel-bg'}`}>
{currentTab === 'INPUT' && hasInputTab && (
<>
<div className="px-4 pb-2 pt-3">
{previewFields.map((field, index) => (
<div
key={field.variable}
className="mb-2 last-of-type:mb-0"
>
<FormItem
autoFocus={index === 0}
className="!block"
payload={field}
value={inputs[field.variable]}
onChange={value => handleValueChange(field.variable, value)}
/>
</div>
))}
</div>
<div className="flex items-center justify-between px-4 py-2">
<Button
variant="primary"
className="w-full"
disabled={workflowRunningData?.result?.status === WorkflowRunningStatus.Running}
onClick={handleSubmit}
>
{t('singleRun.startRun', { ns: 'workflow' })}
</Button>
</div>
</>
)}
{currentTab === 'RESULT' && (
<div className="p-2">
<ResultText
isRunning={workflowRunningData?.result?.status === WorkflowRunningStatus.Running || !workflowRunningData?.result}
outputs={workflowRunningData?.resultText}
allFiles={workflowRunningData?.result?.files}
error={workflowRunningData?.result?.error}
onClick={() => setSelectedTab('DETAIL')}
/>
{(workflowRunningData?.result.status === WorkflowRunningStatus.Succeeded && workflowRunningData?.resultText && typeof workflowRunningData.resultText === 'string') && (
<Button
className="mb-4 ml-4 space-x-1"
onClick={() => {
copy(workflowRunningData?.resultText || '')
toast.success(t('actionMsg.copySuccessfully', { ns: 'common' }))
}}
>
<span className="i-ri-clipboard-line h-3.5 w-3.5" />
<div>{t('operation.copy', { ns: 'common' })}</div>
</Button>
)}
</div>
)}
{currentTab === 'DETAIL' && workflowRunningData?.result && (
<ResultPanel
inputs={workflowRunningData.result?.inputs}
inputs_truncated={workflowRunningData.result?.inputs_truncated}
process_data={workflowRunningData.result?.process_data}
process_data_truncated={workflowRunningData.result?.process_data_truncated}
outputs={workflowRunningData.result?.outputs}
outputs_truncated={workflowRunningData.result?.outputs_truncated}
outputs_full_content={workflowRunningData.result?.outputs_full_content}
status={workflowRunningData.result?.status || ''}
error={workflowRunningData.result?.error}
elapsed_time={workflowRunningData.result?.elapsed_time}
total_tokens={workflowRunningData.result?.total_tokens}
created_at={workflowRunningData.result?.created_at}
created_by={workflowRunningData.result?.created_by}
steps={workflowRunningData.result?.total_steps}
exceptionCounts={workflowRunningData.result?.exceptions_count}
/>
)}
{currentTab === 'DETAIL' && !workflowRunningData?.result && (
<div className="flex h-full items-center justify-center bg-components-panel-bg">
<Loading />
</div>
)}
{currentTab === 'TRACING' && (
<TracingPanel
className="bg-background-section-burn"
list={workflowRunningData?.tracing || []}
/>
)}
{currentTab === 'TRACING' && !workflowRunningData?.tracing?.length && (
<div className="flex h-full items-center justify-center !bg-background-section-burn">
<Loading />
</div>
)}
</div>
</div>
</div>
)
}
export default memo(SnippetRunPanel)

View File

@@ -4,9 +4,18 @@ import type { PanelProps } from '@/app/components/workflow/panel'
import type { SnippetInputField } from '@/models/snippet'
import { memo, useMemo } from 'react'
import Panel from '@/app/components/workflow/panel'
import { useStore } from '@/app/components/workflow/store'
import dynamic from '@/next/dynamic'
import SnippetInputFieldEditor from './input-field-editor'
import SnippetInputFieldPanel from './panel'
const Record = dynamic(() => import('@/app/components/workflow/panel/record'), {
ssr: false,
})
const SnippetRunPanel = dynamic(() => import('./snippet-run-panel'), {
ssr: false,
})
type SnippetWorkflowPanelProps = {
snippetId: string
fields: SnippetInputField[]
@@ -56,6 +65,20 @@ const SnippetPanelOnLeft = ({
)
}
const SnippetPanelOnRight = ({
fields,
}: Pick<SnippetWorkflowPanelProps, 'fields'>) => {
const historyWorkflowData = useStore(s => s.historyWorkflowData)
const showDebugAndPreviewPanel = useStore(s => s.showDebugAndPreviewPanel)
return (
<>
{historyWorkflowData && <Record />}
{showDebugAndPreviewPanel && <SnippetRunPanel fields={fields} />}
</>
)
}
const SnippetWorkflowPanel = ({
snippetId,
fields,
@@ -97,6 +120,7 @@ const SnippetWorkflowPanel = ({
onSortChange={onSortChange}
/>
),
right: <SnippetPanelOnRight fields={fields} />,
},
versionHistoryPanelProps,
}

View File

@@ -0,0 +1,131 @@
import type { SnippetInputField } from '@/models/snippet'
import { renderHook } from '@testing-library/react'
import { act } from 'react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { WorkflowRunningStatus } from '@/app/components/workflow/types'
import { PipelineInputVarType } from '@/models/pipeline'
import { useSnippetStartRun } from '../use-snippet-start-run'
const mockWorkflowStoreGetState = vi.fn()
vi.mock('@/app/components/workflow/store', () => ({
useWorkflowStore: () => ({
getState: mockWorkflowStoreGetState,
}),
}))
const mockHandleCancelDebugAndPreviewPanel = vi.fn()
vi.mock('@/app/components/workflow/hooks', () => ({
useWorkflowInteractions: () => ({
handleCancelDebugAndPreviewPanel: mockHandleCancelDebugAndPreviewPanel,
}),
}))
const mockSetShowDebugAndPreviewPanel = vi.fn()
const mockSetShowInputsPanel = vi.fn()
const mockSetShowEnvPanel = vi.fn()
const mockSetShowGlobalVariablePanel = vi.fn()
const mockHandleRun = vi.fn()
const inputFields: SnippetInputField[] = [
{
type: PipelineInputVarType.textInput,
label: 'Query',
variable: 'query',
required: true,
},
]
describe('useSnippetStartRun', () => {
beforeEach(() => {
vi.clearAllMocks()
mockWorkflowStoreGetState.mockReturnValue({
workflowRunningData: undefined,
showDebugAndPreviewPanel: false,
setShowDebugAndPreviewPanel: mockSetShowDebugAndPreviewPanel,
setShowInputsPanel: mockSetShowInputsPanel,
setShowEnvPanel: mockSetShowEnvPanel,
setShowGlobalVariablePanel: mockSetShowGlobalVariablePanel,
})
})
it('should open the debug panel and input form when snippet has input fields', () => {
const { result } = renderHook(() => useSnippetStartRun({
handleRun: mockHandleRun,
inputFields,
}))
act(() => {
result.current.handleWorkflowStartRunInWorkflow()
})
expect(mockSetShowEnvPanel).toHaveBeenCalledWith(false)
expect(mockSetShowGlobalVariablePanel).toHaveBeenCalledWith(false)
expect(mockSetShowDebugAndPreviewPanel).toHaveBeenCalledWith(true)
expect(mockSetShowInputsPanel).toHaveBeenCalledWith(true)
expect(mockHandleRun).not.toHaveBeenCalled()
})
it('should run immediately when snippet has no input fields', () => {
const { result } = renderHook(() => useSnippetStartRun({
handleRun: mockHandleRun,
inputFields: [],
}))
act(() => {
result.current.handleWorkflowStartRunInWorkflow()
})
expect(mockSetShowDebugAndPreviewPanel).toHaveBeenCalledWith(true)
expect(mockSetShowInputsPanel).toHaveBeenCalledWith(false)
expect(mockHandleRun).toHaveBeenCalledWith({ inputs: {} })
})
it('should close the panel when debug panel is already open', () => {
mockWorkflowStoreGetState.mockReturnValue({
workflowRunningData: undefined,
showDebugAndPreviewPanel: true,
setShowDebugAndPreviewPanel: mockSetShowDebugAndPreviewPanel,
setShowInputsPanel: mockSetShowInputsPanel,
setShowEnvPanel: mockSetShowEnvPanel,
setShowGlobalVariablePanel: mockSetShowGlobalVariablePanel,
})
const { result } = renderHook(() => useSnippetStartRun({
handleRun: mockHandleRun,
inputFields,
}))
act(() => {
result.current.handleWorkflowStartRunInWorkflow()
})
expect(mockHandleCancelDebugAndPreviewPanel).toHaveBeenCalled()
})
it('should do nothing when workflow is already running', () => {
mockWorkflowStoreGetState.mockReturnValue({
workflowRunningData: {
result: {
status: WorkflowRunningStatus.Running,
},
},
showDebugAndPreviewPanel: false,
setShowDebugAndPreviewPanel: mockSetShowDebugAndPreviewPanel,
setShowInputsPanel: mockSetShowInputsPanel,
setShowEnvPanel: mockSetShowEnvPanel,
setShowGlobalVariablePanel: mockSetShowGlobalVariablePanel,
})
const { result } = renderHook(() => useSnippetStartRun({
handleRun: mockHandleRun,
inputFields,
}))
act(() => {
result.current.handleWorkflowStartRunInWorkflow()
})
expect(mockSetShowDebugAndPreviewPanel).not.toHaveBeenCalled()
expect(mockHandleRun).not.toHaveBeenCalled()
})
})

View File

@@ -0,0 +1,298 @@
import type { IOtherOptions } from '@/service/base'
import type { SnippetDraftRunPayload } from '@/types/snippet'
import type { VersionHistory } from '@/types/workflow'
import { produce } from 'immer'
import { useCallback, useRef } from 'react'
import {
useReactFlow,
useStoreApi,
} from 'reactflow'
import { useSetWorkflowVarsWithValue } from '@/app/components/workflow/hooks/use-fetch-workflow-inspect-vars'
import { useWorkflowUpdate } from '@/app/components/workflow/hooks/use-workflow-interactions'
import { useWorkflowRunEvent } from '@/app/components/workflow/hooks/use-workflow-run-event/use-workflow-run-event'
import { useWorkflowStore } from '@/app/components/workflow/store'
import { WorkflowRunningStatus } from '@/app/components/workflow/types'
import { ssePost } from '@/service/base'
import { useInvalidAllLastRun, useInvalidateWorkflowRunHistory } from '@/service/use-workflow'
import { stopWorkflowRun } from '@/service/workflow'
import { FlowType } from '@/types/common'
import { useNodesSyncDraft } from './use-nodes-sync-draft'
export const useSnippetRun = (snippetId: string) => {
const store = useStoreApi()
const workflowStore = useWorkflowStore()
const reactflow = useReactFlow()
const { doSyncWorkflowDraft } = useNodesSyncDraft(snippetId)
const { handleUpdateWorkflowCanvas } = useWorkflowUpdate()
const {
handleWorkflowStarted,
handleWorkflowFinished,
handleWorkflowFailed,
handleWorkflowNodeStarted,
handleWorkflowNodeFinished,
handleWorkflowNodeIterationStarted,
handleWorkflowNodeIterationNext,
handleWorkflowNodeIterationFinished,
handleWorkflowNodeLoopStarted,
handleWorkflowNodeLoopNext,
handleWorkflowNodeLoopFinished,
handleWorkflowNodeRetry,
handleWorkflowAgentLog,
handleWorkflowTextChunk,
handleWorkflowTextReplace,
} = useWorkflowRunEvent()
const abortControllerRef = useRef<AbortController | null>(null)
const handleBackupDraft = useCallback(() => {
const {
getNodes,
edges,
} = store.getState()
const { getViewport } = reactflow
const {
backupDraft,
setBackupDraft,
} = workflowStore.getState()
if (!backupDraft) {
setBackupDraft({
nodes: getNodes(),
edges,
viewport: getViewport(),
environmentVariables: [],
})
doSyncWorkflowDraft()
}
}, [doSyncWorkflowDraft, reactflow, store, workflowStore])
const handleLoadBackupDraft = useCallback(() => {
const {
backupDraft,
setBackupDraft,
setEnvironmentVariables,
} = workflowStore.getState()
if (backupDraft) {
const {
nodes,
edges,
viewport,
} = backupDraft
handleUpdateWorkflowCanvas({
nodes,
edges,
viewport,
})
setEnvironmentVariables([])
setBackupDraft(undefined)
}
}, [handleUpdateWorkflowCanvas, workflowStore])
const invalidAllLastRun = useInvalidAllLastRun(FlowType.snippet, snippetId)
const invalidateRunHistory = useInvalidateWorkflowRunHistory()
const { fetchInspectVars } = useSetWorkflowVarsWithValue({
flowType: FlowType.snippet,
flowId: snippetId,
})
const handleRun = useCallback(async (
params: SnippetDraftRunPayload,
callback?: IOtherOptions,
) => {
const {
getNodes,
setNodes,
} = store.getState()
const newNodes = produce(getNodes(), (draft) => {
draft.forEach((node) => {
node.data.selected = false
node.data._runningStatus = undefined
})
})
setNodes(newNodes)
await doSyncWorkflowDraft()
const {
onWorkflowStarted,
onWorkflowFinished,
onNodeStarted,
onNodeFinished,
onIterationStart,
onIterationNext,
onIterationFinish,
onLoopStart,
onLoopNext,
onLoopFinish,
onNodeRetry,
onAgentLog,
onError,
...restCallback
} = callback || {}
const runHistoryUrl = `/snippets/${snippetId}/workflow-runs`
workflowStore.setState({ historyWorkflowData: undefined })
const workflowContainer = document.getElementById('workflow-container')
const {
clientWidth,
clientHeight,
} = workflowContainer!
const url = `/snippets/${snippetId}/workflows/draft/run`
const {
setWorkflowRunningData,
} = workflowStore.getState()
setWorkflowRunningData({
result: {
inputs_truncated: false,
process_data_truncated: false,
outputs_truncated: false,
status: WorkflowRunningStatus.Running,
},
tracing: [],
resultText: '',
})
abortControllerRef.current?.abort()
abortControllerRef.current = null
ssePost(
url,
{
body: params,
},
{
getAbortController: (controller: AbortController) => {
abortControllerRef.current = controller
},
onWorkflowStarted: (params) => {
handleWorkflowStarted(params)
invalidateRunHistory(runHistoryUrl)
onWorkflowStarted?.(params)
},
onWorkflowFinished: (params) => {
handleWorkflowFinished(params)
invalidateRunHistory(runHistoryUrl)
fetchInspectVars({})
invalidAllLastRun()
onWorkflowFinished?.(params)
},
onError: (params) => {
handleWorkflowFailed()
invalidateRunHistory(runHistoryUrl)
onError?.(params)
},
onNodeStarted: (params) => {
handleWorkflowNodeStarted(
params,
{
clientWidth,
clientHeight,
},
)
onNodeStarted?.(params)
},
onNodeFinished: (params) => {
handleWorkflowNodeFinished(params)
onNodeFinished?.(params)
},
onIterationStart: (params) => {
handleWorkflowNodeIterationStarted(
params,
{
clientWidth,
clientHeight,
},
)
onIterationStart?.(params)
},
onIterationNext: (params) => {
handleWorkflowNodeIterationNext(params)
onIterationNext?.(params)
},
onIterationFinish: (params) => {
handleWorkflowNodeIterationFinished(params)
onIterationFinish?.(params)
},
onLoopStart: (params) => {
handleWorkflowNodeLoopStarted(
params,
{
clientWidth,
clientHeight,
},
)
onLoopStart?.(params)
},
onLoopNext: (params) => {
handleWorkflowNodeLoopNext(params)
onLoopNext?.(params)
},
onLoopFinish: (params) => {
handleWorkflowNodeLoopFinished(params)
onLoopFinish?.(params)
},
onNodeRetry: (params) => {
handleWorkflowNodeRetry(params)
onNodeRetry?.(params)
},
onAgentLog: (params) => {
handleWorkflowAgentLog(params)
onAgentLog?.(params)
},
onTextChunk: (params) => {
handleWorkflowTextChunk(params)
},
onTextReplace: (params) => {
handleWorkflowTextReplace(params)
},
...restCallback,
},
)
}, [doSyncWorkflowDraft, fetchInspectVars, handleWorkflowAgentLog, handleWorkflowFailed, handleWorkflowFinished, handleWorkflowNodeFinished, handleWorkflowNodeIterationFinished, handleWorkflowNodeIterationNext, handleWorkflowNodeIterationStarted, handleWorkflowNodeLoopFinished, handleWorkflowNodeLoopNext, handleWorkflowNodeLoopStarted, handleWorkflowNodeRetry, handleWorkflowNodeStarted, handleWorkflowStarted, handleWorkflowTextChunk, handleWorkflowTextReplace, invalidAllLastRun, invalidateRunHistory, snippetId, store, workflowStore])
const handleStopRun = useCallback((taskId: string) => {
stopWorkflowRun(`/snippets/${snippetId}/workflow-runs/tasks/${taskId}/stop`)
if (abortControllerRef.current)
abortControllerRef.current.abort()
abortControllerRef.current = null
}, [snippetId])
const handleRestoreFromPublishedWorkflow = useCallback((publishedWorkflow: VersionHistory) => {
const nodes = publishedWorkflow.graph.nodes.map(node => ({ ...node, selected: false, data: { ...node.data, selected: false } }))
const edges = publishedWorkflow.graph.edges
const viewport = publishedWorkflow.graph.viewport!
handleUpdateWorkflowCanvas({
nodes,
edges,
viewport,
})
workflowStore.getState().setEnvironmentVariables([])
}, [handleUpdateWorkflowCanvas, workflowStore])
return {
handleBackupDraft,
handleLoadBackupDraft,
handleRun,
handleStopRun,
handleRestoreFromPublishedWorkflow,
}
}

View File

@@ -0,0 +1,60 @@
import type { SnippetInputField } from '@/models/snippet'
import type { SnippetDraftRunPayload } from '@/types/snippet'
import { useCallback } from 'react'
import { useWorkflowInteractions } from '@/app/components/workflow/hooks'
import { useWorkflowStore } from '@/app/components/workflow/store'
import { WorkflowRunningStatus } from '@/app/components/workflow/types'
type UseSnippetStartRunOptions = {
handleRun: (params: SnippetDraftRunPayload) => void
inputFields: SnippetInputField[]
}
export const useSnippetStartRun = ({
handleRun,
inputFields,
}: UseSnippetStartRunOptions) => {
const workflowStore = useWorkflowStore()
const { handleCancelDebugAndPreviewPanel } = useWorkflowInteractions()
const handleWorkflowStartRunInWorkflow = useCallback(() => {
const {
workflowRunningData,
showDebugAndPreviewPanel,
setShowDebugAndPreviewPanel,
setShowInputsPanel,
setShowEnvPanel,
setShowGlobalVariablePanel,
} = workflowStore.getState()
if (workflowRunningData?.result.status === WorkflowRunningStatus.Running)
return
setShowEnvPanel(false)
setShowGlobalVariablePanel(false)
if (showDebugAndPreviewPanel) {
handleCancelDebugAndPreviewPanel()
return
}
setShowDebugAndPreviewPanel(true)
if (inputFields.length > 0) {
setShowInputsPanel(true)
return
}
setShowInputsPanel(false)
handleRun({ inputs: {} })
}, [handleCancelDebugAndPreviewPanel, handleRun, inputFields.length, workflowStore])
const handleStartWorkflowRun = useCallback(() => {
handleWorkflowStartRunInWorkflow()
}, [handleWorkflowStartRunInWorkflow])
return {
handleStartWorkflowRun,
handleWorkflowStartRunInWorkflow,
}
}