From 768b3eb6f9f8023e743451bfbf511a0fabedc520 Mon Sep 17 00:00:00 2001 From: JzoNg Date: Sun, 29 Mar 2026 20:55:11 +0800 Subject: [PATCH] feat(web): test run of snippet --- .../__tests__/snippet-main.spec.tsx | 38 +++ .../__tests__/workflow-panel.spec.tsx | 1 + .../snippets/components/snippet-main.tsx | 30 ++ .../snippets/components/snippet-run-panel.tsx | 293 +++++++++++++++++ .../snippets/components/workflow-panel.tsx | 24 ++ .../__tests__/use-snippet-start-run.spec.ts | 131 ++++++++ .../snippets/hooks/use-snippet-run.ts | 298 ++++++++++++++++++ .../snippets/hooks/use-snippet-start-run.ts | 60 ++++ 8 files changed, 875 insertions(+) create mode 100644 web/app/components/snippets/components/snippet-run-panel.tsx create mode 100644 web/app/components/snippets/hooks/__tests__/use-snippet-start-run.spec.ts create mode 100644 web/app/components/snippets/hooks/use-snippet-run.ts create mode 100644 web/app/components/snippets/hooks/use-snippet-start-run.ts diff --git a/web/app/components/snippets/components/__tests__/snippet-main.spec.tsx b/web/app/components/snippets/components/__tests__/snippet-main.spec.tsx index edcc4c1bc0..ff3a3961ef 100644 --- a/web/app/components/snippets/components/__tests__/snippet-main.spec.tsx +++ b/web/app/components/snippets/components/__tests__/snippet-main.spec.tsx @@ -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) + }) + }) }) diff --git a/web/app/components/snippets/components/__tests__/workflow-panel.spec.tsx b/web/app/components/snippets/components/__tests__/workflow-panel.spec.tsx index 7b81a98494..db8a333143 100644 --- a/web/app/components/snippets/components/__tests__/workflow-panel.spec.tsx +++ b/web/app/components/snippets/components/__tests__/workflow-panel.spec.tsx @@ -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() }) }) }) diff --git a/web/app/components/snippets/components/snippet-main.tsx b/web/app/components/snippets/components/snippet-main.tsx index b6f3eb1f9f..46740c344a 100644 --- a/web/app/components/snippets/components/snippet-main.tsx +++ b/web/app/components/snippets/components/snippet-main.tsx @@ -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, diff --git a/web/app/components/snippets/components/snippet-run-panel.tsx b/web/app/components/snippets/components/snippet-run-panel.tsx new file mode 100644 index 0000000000..912342d732 --- /dev/null +++ b/web/app/components/snippets/components/snippet-run-panel.tsx @@ -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.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>((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 | null>(null) + const [selectedTab, setSelectedTab] = useState(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 ( +
+
+
+ {`Test Run${formatWorkflowRunIdentifier(workflowRunningData?.result.finished_at, workflowRunningData?.result.status)}`} +
+ +
+
+
+
+ {hasInputTab && ( +
setSelectedTab('INPUT')} + > + {t('input', { ns: 'runLog' })} +
+ )} +
workflowRunningData && setSelectedTab('RESULT')} + > + {t('result', { ns: 'runLog' })} +
+
workflowRunningData && setSelectedTab('DETAIL')} + > + {t('detail', { ns: 'runLog' })} +
+
workflowRunningData && setSelectedTab('TRACING')} + > + {t('tracing', { ns: 'runLog' })} +
+
+
+ {currentTab === 'INPUT' && hasInputTab && ( + <> +
+ {previewFields.map((field, index) => ( +
+ handleValueChange(field.variable, value)} + /> +
+ ))} +
+
+ +
+ + )} + {currentTab === 'RESULT' && ( +
+ setSelectedTab('DETAIL')} + /> + {(workflowRunningData?.result.status === WorkflowRunningStatus.Succeeded && workflowRunningData?.resultText && typeof workflowRunningData.resultText === 'string') && ( + + )} +
+ )} + {currentTab === 'DETAIL' && workflowRunningData?.result && ( + + )} + {currentTab === 'DETAIL' && !workflowRunningData?.result && ( +
+ +
+ )} + {currentTab === 'TRACING' && ( + + )} + {currentTab === 'TRACING' && !workflowRunningData?.tracing?.length && ( +
+ +
+ )} +
+
+
+ ) +} + +export default memo(SnippetRunPanel) diff --git a/web/app/components/snippets/components/workflow-panel.tsx b/web/app/components/snippets/components/workflow-panel.tsx index 80c41df8a0..cb2f281c5d 100644 --- a/web/app/components/snippets/components/workflow-panel.tsx +++ b/web/app/components/snippets/components/workflow-panel.tsx @@ -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) => { + const historyWorkflowData = useStore(s => s.historyWorkflowData) + const showDebugAndPreviewPanel = useStore(s => s.showDebugAndPreviewPanel) + + return ( + <> + {historyWorkflowData && } + {showDebugAndPreviewPanel && } + + ) +} + const SnippetWorkflowPanel = ({ snippetId, fields, @@ -97,6 +120,7 @@ const SnippetWorkflowPanel = ({ onSortChange={onSortChange} /> ), + right: , }, versionHistoryPanelProps, } diff --git a/web/app/components/snippets/hooks/__tests__/use-snippet-start-run.spec.ts b/web/app/components/snippets/hooks/__tests__/use-snippet-start-run.spec.ts new file mode 100644 index 0000000000..909110939a --- /dev/null +++ b/web/app/components/snippets/hooks/__tests__/use-snippet-start-run.spec.ts @@ -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() + }) +}) diff --git a/web/app/components/snippets/hooks/use-snippet-run.ts b/web/app/components/snippets/hooks/use-snippet-run.ts new file mode 100644 index 0000000000..3c8668270e --- /dev/null +++ b/web/app/components/snippets/hooks/use-snippet-run.ts @@ -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(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, + } +} diff --git a/web/app/components/snippets/hooks/use-snippet-start-run.ts b/web/app/components/snippets/hooks/use-snippet-start-run.ts new file mode 100644 index 0000000000..27051510b9 --- /dev/null +++ b/web/app/components/snippets/hooks/use-snippet-start-run.ts @@ -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, + } +}