From 449755ada41daaa199c993ea2e72e50b324932b2 Mon Sep 17 00:00:00 2001 From: twwu Date: Wed, 20 Aug 2025 16:40:56 +0800 Subject: [PATCH] refactor(test-run, preparation): restructure test run components, enhance data handling, and improve user experience with new loading states and error handling --- .../documents/create-from-pipeline/hooks.ts | 2 +- .../documents/create-from-pipeline/index.tsx | 40 +-- .../components/chunk-card-list/index.tsx | 200 ++++++++------- .../panel/input-field/preview/data-source.tsx | 2 +- .../panel/test-run/close-button.tsx | 22 -- .../components/panel/test-run/header.tsx | 38 ++- .../components/panel/test-run/index.tsx | 228 ++---------------- .../{ => preparation}/actions/index.tsx | 0 .../data-source-options/index.tsx | 3 +- .../data-source-options/option-card.tsx | 0 .../document-processing/actions.tsx | 0 .../document-processing/hooks.ts | 0 .../document-processing/index.tsx | 0 .../document-processing/options.tsx | 0 .../{ => preparation}/footer-tips.tsx | 0 .../panel/test-run/{ => preparation}/hooks.ts | 4 +- .../panel/test-run/preparation/index.tsx | 213 ++++++++++++++++ .../{ => preparation}/step-indicator.tsx | 4 +- .../panel/test-run/result/index.tsx | 71 ++++++ .../test-run/result/result-preview/index.tsx | 65 +++++ .../panel/test-run/result/tabs/index.tsx | 45 ++++ .../panel/test-run/result/tabs/tab.tsx | 34 +++ .../components/rag-pipeline-header/index.tsx | 17 +- .../rag-pipeline-header/run-mode.tsx | 117 +++++++++ .../rag-pipeline/hooks/use-pipeline-run.ts | 3 +- .../hooks/use-pipeline-start-run.tsx | 12 + .../components/rag-pipeline/store/index.ts | 4 + .../workflow/header/run-and-history.tsx | 98 +------- .../components/workflow/header/run-mode.tsx | 96 ++++++++ .../variable-inspect/value-content.tsx | 2 +- web/i18n/en-US/pipeline.ts | 12 + web/i18n/en-US/workflow.ts | 6 +- web/i18n/zh-Hans/pipeline.ts | 12 + web/i18n/zh-Hans/workflow.ts | 18 +- 34 files changed, 889 insertions(+), 479 deletions(-) delete mode 100644 web/app/components/rag-pipeline/components/panel/test-run/close-button.tsx rename web/app/components/rag-pipeline/components/panel/test-run/{ => preparation}/actions/index.tsx (100%) rename web/app/components/rag-pipeline/components/panel/test-run/{ => preparation}/data-source-options/index.tsx (92%) rename web/app/components/rag-pipeline/components/panel/test-run/{ => preparation}/data-source-options/option-card.tsx (100%) rename web/app/components/rag-pipeline/components/panel/test-run/{ => preparation}/document-processing/actions.tsx (100%) rename web/app/components/rag-pipeline/components/panel/test-run/{ => preparation}/document-processing/hooks.ts (100%) rename web/app/components/rag-pipeline/components/panel/test-run/{ => preparation}/document-processing/index.tsx (100%) rename web/app/components/rag-pipeline/components/panel/test-run/{ => preparation}/document-processing/options.tsx (100%) rename web/app/components/rag-pipeline/components/panel/test-run/{ => preparation}/footer-tips.tsx (100%) rename web/app/components/rag-pipeline/components/panel/test-run/{ => preparation}/hooks.ts (97%) create mode 100644 web/app/components/rag-pipeline/components/panel/test-run/preparation/index.tsx rename web/app/components/rag-pipeline/components/panel/test-run/{ => preparation}/step-indicator.tsx (87%) create mode 100644 web/app/components/rag-pipeline/components/panel/test-run/result/index.tsx create mode 100644 web/app/components/rag-pipeline/components/panel/test-run/result/result-preview/index.tsx create mode 100644 web/app/components/rag-pipeline/components/panel/test-run/result/tabs/index.tsx create mode 100644 web/app/components/rag-pipeline/components/panel/test-run/result/tabs/tab.tsx create mode 100644 web/app/components/rag-pipeline/components/rag-pipeline-header/run-mode.tsx create mode 100644 web/app/components/workflow/header/run-mode.tsx diff --git a/web/app/components/datasets/documents/create-from-pipeline/hooks.ts b/web/app/components/datasets/documents/create-from-pipeline/hooks.ts index c05bceb7e2..fb24ce5925 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/hooks.ts +++ b/web/app/components/datasets/documents/create-from-pipeline/hooks.ts @@ -196,7 +196,7 @@ export const useOnlineDrive = () => { const dataSourceStore = useDataSourceStore() const selectedOnlineDriveFileList = useMemo(() => { - return selectedFileIds.map(key => fileList.find(item => item.key === key)!) + return selectedFileIds.map(key => fileList.find(item => item.id === key)!) }, [fileList, selectedFileIds]) const clearOnlineDriveData = useCallback(() => { diff --git a/web/app/components/datasets/documents/create-from-pipeline/index.tsx b/web/app/components/datasets/documents/create-from-pipeline/index.tsx index 4a25c357eb..b679051cf3 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/index.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/index.tsx @@ -87,7 +87,7 @@ const CreateFormPipeline = () => { clearOnlineDriveData, } = useOnlineDrive() - const datasourceType = datasource?.nodeData.provider_type + const datasourceType = useMemo(() => datasource?.nodeData.provider_type, [datasource]) const isVectorSpaceFull = plan.usage.vectorSpace >= plan.total.vectorSpace const isShowVectorSpaceFull = useMemo(() => { if (!datasource) @@ -234,10 +234,13 @@ const CreateFormPipeline = () => { const handleProcess = useCallback(async (data: Record) => { if (!datasource) return - const { bucket, currentCredentialId, fileList: onlineDriveFileList } = dataSourceStore.getState() + const { currentCredentialId } = dataSourceStore.getState() const datasourceInfoList: Record[] = [] if (datasourceType === DatasourceType.localFile) { - fileList.forEach((file) => { + const { + localFileList, + } = dataSourceStore.getState() + localFileList.forEach((file) => { const { id, name, type, size, extension, mime_type } = file.file const documentInfo = { related_id: id, @@ -254,6 +257,9 @@ const CreateFormPipeline = () => { }) } if (datasourceType === DatasourceType.onlineDocument) { + const { + onlineDocuments, + } = dataSourceStore.getState() onlineDocuments.forEach((page) => { const { workspace_id, ...rest } = page const documentInfo = { @@ -265,6 +271,9 @@ const CreateFormPipeline = () => { }) } if (datasourceType === DatasourceType.websiteCrawl) { + const { + websitePages, + } = dataSourceStore.getState() websitePages.forEach((websitePage) => { datasourceInfoList.push({ ...websitePage, @@ -273,17 +282,20 @@ const CreateFormPipeline = () => { }) } if (datasourceType === DatasourceType.onlineDrive) { - if (datasourceType === DatasourceType.onlineDrive) { - selectedFileIds.forEach((id) => { - const file = onlineDriveFileList.find(file => file.id === id) - datasourceInfoList.push({ - bucket, - id: file?.id, - type: file?.type, - credential_id: currentCredentialId, - }) + const { + bucket, + selectedFileIds, + fileList: onlineDriveFileList, + } = dataSourceStore.getState() + selectedFileIds.forEach((id) => { + const file = onlineDriveFileList.find(file => file.id === id) + datasourceInfoList.push({ + bucket, + id: file?.id, + type: file?.type, + credential_id: currentCredentialId, }) - } + }) } await runPublishedPipeline({ pipeline_id: pipelineId!, @@ -299,7 +311,7 @@ const CreateFormPipeline = () => { handleNextStep() }, }) - }, [dataSourceStore, datasource, datasourceType, fileList, handleNextStep, onlineDocuments, pipelineId, runPublishedPipeline, selectedFileIds, websitePages]) + }, [dataSourceStore, datasource, datasourceType, handleNextStep, pipelineId, runPublishedPipeline]) const onClickProcess = useCallback(() => { isPreview.current = false diff --git a/web/app/components/rag-pipeline/components/chunk-card-list/index.tsx b/web/app/components/rag-pipeline/components/chunk-card-list/index.tsx index 92ae981cf2..9cba8a5e93 100644 --- a/web/app/components/rag-pipeline/components/chunk-card-list/index.tsx +++ b/web/app/components/rag-pipeline/components/chunk-card-list/index.tsx @@ -4,138 +4,154 @@ import Dot from '@/app/components/datasets/documents/detail/completed/common/dot import { PreviewSlice } from '@/app/components/datasets/formatted-text/flavours/preview-slice' import { useTranslation } from 'react-i18next' import { formatNumber } from '@/utils/format' +import cn from '@/utils/classnames' enum QAItemType { - Question = 'question', - Answer = 'answer', + Question = 'question', + Answer = 'answer', } type QAItemProps = { - type: QAItemType - text: string + type: QAItemType + text: string } const QAItem = (props: QAItemProps) => { - const { type, text } = props - return
-
{type === QAItemType.Question ? 'Q' : 'A'}
-
{text}
-
+ const { type, text } = props + return
+
{type === QAItemType.Question ? 'Q' : 'A'}
+
{text}
+
} enum ChunkType { - General = 'genaral', - Paragraph = 'paragraph', - FullDoc = 'full-doc', - QA = 'qa', + General = 'general', + Paragraph = 'paragraph', + FullDoc = 'full-doc', + QA = 'qa', } type ChunkCardProps = { - type: ChunkType - content: string | string[] | QAChunk - positionId?: string | number - wordCount: number + type: ChunkType + content: string | string[] | QAChunk + positionId?: string | number + wordCount: number } const ChunkCard = (props: ChunkCardProps) => { - const { type, content, positionId, wordCount } = props - const { t } = useTranslation() + const { type, content, positionId, wordCount } = props + const { t } = useTranslation() - const renderContent = () => { - // ChunkType.Paragraph && ChunkType.FullDoc - if (Array.isArray(content)) { - return content.map((child, index) => { - const indexForLabel = index + 1 - return ( - - ) - }) - } - - // ChunkType.QA - if (typeof content === 'object') { - return
- - -
- } - - // ChunkType.General - return content + const renderContent = () => { + // ChunkType.Paragraph && ChunkType.FullDoc + if (Array.isArray(content)) { + return content.map((child, index) => { + const indexForLabel = index + 1 + return ( + + ) + }) } - return
- {type !== ChunkType.FullDoc &&
- - -
{formatNumber(wordCount)} {t('datasetDocuments.segment.characters', { count: wordCount })}
-
} -
{renderContent()}
+ // ChunkType.QA + if (typeof content === 'object') { + return
+ + +
+ } + + // ChunkType.General + return content + } + + return ( +
+ {type !== ChunkType.FullDoc &&
+ + +
{formatNumber(wordCount)} {t('datasetDocuments.segment.characters', { count: wordCount })}
+
} +
{renderContent()}
+ ) } export type ChunkInfo = { - general_chunks?: string[] - parent_child_chunks?: ParentChildChunk[] - parent_mode?: string - qa_chunks?: QAChunk[] + general_chunks?: string[] + parent_child_chunks?: ParentChildChunk[] + parent_mode?: string + qa_chunks?: QAChunk[] } type ParentChildChunk = { - child_contents: string[] - parent_content: string - parent_mode: string + child_contents: string[] + parent_content: string + parent_mode: string } type QAChunk = { - question: string - answer: string + question: string + answer: string } type ChunkCardListProps = { - chunkInfo: ChunkInfo + chunkInfo: ChunkInfo + className?: string } export const ChunkCardList = (props: ChunkCardListProps) => { - const { chunkInfo } = props + const { chunkInfo, className } = props - const chunkType = useMemo(() => { - if (chunkInfo?.general_chunks) - return ChunkType.General + const chunkType = useMemo(() => { + if (chunkInfo?.general_chunks) + return ChunkType.General - if (chunkInfo?.parent_child_chunks) - return chunkInfo.parent_mode as ChunkType + if (chunkInfo?.parent_child_chunks) + return chunkInfo.parent_mode as ChunkType - return ChunkType.QA - }, [chunkInfo]) + return ChunkType.QA + }, [chunkInfo]) - return
- {(chunkInfo.general_chunks ?? chunkInfo.parent_child_chunks ?? chunkInfo?.qa_chunks ?? []).map((seg: string | ParentChildChunk | QAChunk, index: number) => { - const isParentChildMode = [ChunkType.Paragraph, ChunkType.FullDoc].includes(chunkType!) - let wordCount = 0 - if (isParentChildMode) - wordCount = (seg as ParentChildChunk)?.parent_content?.length - else if (typeof seg === 'string') - wordCount = seg.length - else - wordCount = (seg as QAChunk)?.question?.length + (seg as QAChunk)?.answer?.length + const chunkList = useMemo(() => { + if (chunkInfo?.general_chunks) + return chunkInfo.general_chunks + if (chunkInfo?.parent_child_chunks) + return chunkInfo.parent_child_chunks + return chunkInfo?.qa_chunks ?? [] + }, [chunkInfo]) - return - })} + return ( +
+ {chunkList.map((seg: string | ParentChildChunk | QAChunk, index: number) => { + const isParentChildMode = [ChunkType.Paragraph, ChunkType.FullDoc].includes(chunkType!) + let wordCount = 0 + if (isParentChildMode) + wordCount = (seg as ParentChildChunk)?.parent_content?.length + else if (typeof seg === 'string') + wordCount = seg.length + else + wordCount = (seg as QAChunk)?.question?.length + (seg as QAChunk)?.answer?.length + + return ( + + ) + })}
+ ) } diff --git a/web/app/components/rag-pipeline/components/panel/input-field/preview/data-source.tsx b/web/app/components/rag-pipeline/components/panel/input-field/preview/data-source.tsx index d6355c5d8a..ba22a37820 100644 --- a/web/app/components/rag-pipeline/components/panel/input-field/preview/data-source.tsx +++ b/web/app/components/rag-pipeline/components/panel/input-field/preview/data-source.tsx @@ -1,6 +1,6 @@ import React from 'react' import { useTranslation } from 'react-i18next' -import DataSourceOptions from '../../test-run/data-source-options' +import DataSourceOptions from '../../test-run/preparation/data-source-options' import Form from './form' import type { Datasource } from '../../test-run/types' import { useStore } from '@/app/components/workflow/store' diff --git a/web/app/components/rag-pipeline/components/panel/test-run/close-button.tsx b/web/app/components/rag-pipeline/components/panel/test-run/close-button.tsx deleted file mode 100644 index cdca16323a..0000000000 --- a/web/app/components/rag-pipeline/components/panel/test-run/close-button.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import React from 'react' -import { RiCloseLine } from '@remixicon/react' - -type CloseButtonProps = { - handleClose: () => void -} - -const CloseButton = ({ - handleClose, -}: CloseButtonProps) => { - return ( - - ) -} - -export default React.memo(CloseButton) diff --git a/web/app/components/rag-pipeline/components/panel/test-run/header.tsx b/web/app/components/rag-pipeline/components/panel/test-run/header.tsx index 953c7ed7f8..16291f868b 100644 --- a/web/app/components/rag-pipeline/components/panel/test-run/header.tsx +++ b/web/app/components/rag-pipeline/components/panel/test-run/header.tsx @@ -1,24 +1,36 @@ -import React from 'react' +import React, { useCallback } from 'react' import { useTranslation } from 'react-i18next' -import StepIndicator from './step-indicator' +import { useWorkflowStore } from '@/app/components/workflow/store' +import { useWorkflowInteractions } from '@/app/components/workflow/hooks' +import { RiCloseLine } from '@remixicon/react' -type HeaderProps = { - steps: { label: string; value: string }[] - currentStep: number -} +const Header = () => { + const workflowStore = useWorkflowStore() -const Header = ({ - steps, - currentStep, -}: HeaderProps) => { const { t } = useTranslation() + const { handleCancelDebugAndPreviewPanel } = useWorkflowInteractions() + + const handleClose = useCallback(() => { + const { + isPreparingDataSource, + setIsPreparingDataSource, + } = workflowStore.getState() + isPreparingDataSource && setIsPreparingDataSource?.(false) + handleCancelDebugAndPreviewPanel() + }, [workflowStore]) return ( -
-
+
+
{t('datasetPipeline.testRun.title')}
- +
) } diff --git a/web/app/components/rag-pipeline/components/panel/test-run/index.tsx b/web/app/components/rag-pipeline/components/panel/test-run/index.tsx index 9b5dc0efee..2620adca52 100644 --- a/web/app/components/rag-pipeline/components/panel/test-run/index.tsx +++ b/web/app/components/rag-pipeline/components/panel/test-run/index.tsx @@ -1,226 +1,26 @@ -import { useStore as useWorkflowStoreWithSelector } from '@/app/components/workflow/store' -import { useCallback, useMemo, useState } from 'react' -import { - useOnlineDocument, - useOnlineDrive, - useTestRunSteps, - useWebsiteCrawl, -} from './hooks' -import DataSourceOptions from './data-source-options' -import LocalFile from '@/app/components/datasets/documents/create-from-pipeline/data-source/local-file' -import OnlineDocuments from '@/app/components/datasets/documents/create-from-pipeline/data-source/online-documents' -import WebsiteCrawl from '@/app/components/datasets/documents/create-from-pipeline/data-source/website-crawl' -import OnlineDrive from '@/app/components/datasets/documents/create-from-pipeline/data-source/online-drive' -import Actions from './actions' -import DocumentProcessing from './document-processing' -import { useWorkflowRun } from '@/app/components/workflow/hooks' -import type { Datasource } from './types' -import { DatasourceType } from '@/models/pipeline' -import { TransferMethod } from '@/types/app' -import CloseButton from './close-button' -import Header from './header' -import FooterTips from './footer-tips' +import { useStore } from '@/app/components/workflow/store' import DataSourceProvider from '@/app/components/datasets/documents/create-from-pipeline/data-source/store/provider' -import { useDataSourceStore, useDataSourceStoreWithSelector } from '@/app/components/datasets/documents/create-from-pipeline/data-source/store' -import { useShallow } from 'zustand/react/shallow' +import Preparation from './preparation' +import Result from './result' +import Header from './header' const TestRunPanel = () => { - const setShowDebugAndPreviewPanel = useWorkflowStoreWithSelector(state => state.setShowDebugAndPreviewPanel) - const { - localFileList: fileList, - onlineDocuments, - websitePages, - selectedFileIds, - } = useDataSourceStoreWithSelector(useShallow(state => ({ - localFileList: state.localFileList, - onlineDocuments: state.onlineDocuments, - websitePages: state.websitePages, - selectedFileIds: state.selectedFileIds, - }))) - const dataSourceStore = useDataSourceStore() - const [datasource, setDatasource] = useState() - - const { - steps, - currentStep, - handleNextStep, - handleBackStep, - } = useTestRunSteps() - - const { clearOnlineDocumentData } = useOnlineDocument() - const { clearWebsiteCrawlData } = useWebsiteCrawl() - const { clearOnlineDriveData } = useOnlineDrive() - - const datasourceType = datasource?.nodeData.provider_type - - const nextBtnDisabled = useMemo(() => { - if (!datasource) return true - if (datasourceType === DatasourceType.localFile) - return !fileList.length || fileList.some(file => !file.file.id) - if (datasourceType === DatasourceType.onlineDocument) - return !onlineDocuments.length - if (datasourceType === DatasourceType.websiteCrawl) - return !websitePages.length - if (datasourceType === DatasourceType.onlineDrive) - return !selectedFileIds.length - return false - }, [datasource, datasourceType, fileList, onlineDocuments.length, selectedFileIds.length, websitePages.length]) - - const handleClose = useCallback(() => { - setShowDebugAndPreviewPanel(false) - }, [setShowDebugAndPreviewPanel]) - - const { handleRun } = useWorkflowRun() - - const handleProcess = useCallback((data: Record) => { - if (!datasource) - return - const datasourceInfoList: Record[] = [] - const credentialId = dataSourceStore.getState().currentCredentialId - if (datasourceType === DatasourceType.localFile) { - const { id, name, type, size, extension, mime_type } = fileList[0].file - const documentInfo = { - related_id: id, - name, - type, - size, - extension, - mime_type, - url: '', - transfer_method: TransferMethod.local_file, - } - datasourceInfoList.push(documentInfo) - } - if (datasourceType === DatasourceType.onlineDocument) { - const { workspace_id, ...rest } = onlineDocuments[0] - const documentInfo = { - workspace_id, - page: rest, - credential_id: credentialId, - } - datasourceInfoList.push(documentInfo) - } - if (datasourceType === DatasourceType.websiteCrawl) { - datasourceInfoList.push({ - ...websitePages[0], - credential_id: credentialId, - }) - } - if (datasourceType === DatasourceType.onlineDrive) { - const { bucket, fileList } = dataSourceStore.getState() - const file = fileList.find(file => file.id === selectedFileIds[0]) - datasourceInfoList.push({ - bucket, - id: file?.id, - type: file?.type, - credential_id: credentialId, - }) - } - handleRun({ - inputs: data, - start_node_id: datasource.nodeId, - datasource_type: datasourceType, - datasource_info_list: datasourceInfoList, - }) - }, [dataSourceStore, datasource, datasourceType, fileList, handleRun, onlineDocuments, selectedFileIds, websitePages]) - - const clearDataSourceData = useCallback((dataSource: Datasource) => { - if (dataSource.nodeData.provider_type === DatasourceType.onlineDocument) - clearOnlineDocumentData() - else if (dataSource.nodeData.provider_type === DatasourceType.websiteCrawl) - clearWebsiteCrawlData() - else if (dataSource.nodeData.provider_type === DatasourceType.onlineDrive) - clearOnlineDriveData() - }, []) - - const handleSwitchDataSource = useCallback((dataSource: Datasource) => { - const { - setCurrentCredentialId, - currentNodeIdRef, - } = dataSourceStore.getState() - clearDataSourceData(dataSource) - setCurrentCredentialId('') - currentNodeIdRef.current = dataSource.nodeId - setDatasource(dataSource) - }, [dataSourceStore]) - - const handleCredentialChange = useCallback((credentialId: string) => { - const { setCurrentCredentialId } = dataSourceStore.getState() - clearDataSourceData(datasource!) - setCurrentCredentialId(credentialId) - }, [dataSourceStore, datasource]) + const isPreparingDataSource = useStore(state => state.isPreparingDataSource) return (
- -
-
- { - currentStep === 1 && ( - <> -
- - {datasourceType === DatasourceType.localFile && ( - - )} - {datasourceType === DatasourceType.onlineDocument && ( - - )} - {datasourceType === DatasourceType.websiteCrawl && ( - - )} - {datasourceType === DatasourceType.onlineDrive && ( - - )} -
- - - - ) - } - { - currentStep === 2 && ( - - ) - } -
+
+ {isPreparingDataSource ? ( + + + + ) : ( + + )}
) } -const TestRunPanelWrapper = () => { - return ( - - - - ) -} - -export default TestRunPanelWrapper +export default TestRunPanel diff --git a/web/app/components/rag-pipeline/components/panel/test-run/actions/index.tsx b/web/app/components/rag-pipeline/components/panel/test-run/preparation/actions/index.tsx similarity index 100% rename from web/app/components/rag-pipeline/components/panel/test-run/actions/index.tsx rename to web/app/components/rag-pipeline/components/panel/test-run/preparation/actions/index.tsx diff --git a/web/app/components/rag-pipeline/components/panel/test-run/data-source-options/index.tsx b/web/app/components/rag-pipeline/components/panel/test-run/preparation/data-source-options/index.tsx similarity index 92% rename from web/app/components/rag-pipeline/components/panel/test-run/data-source-options/index.tsx rename to web/app/components/rag-pipeline/components/panel/test-run/preparation/data-source-options/index.tsx index 717138dc2f..4742b5bb23 100644 --- a/web/app/components/rag-pipeline/components/panel/test-run/data-source-options/index.tsx +++ b/web/app/components/rag-pipeline/components/panel/test-run/preparation/data-source-options/index.tsx @@ -1,7 +1,7 @@ import { useCallback, useEffect } from 'react' import { useDatasourceOptions } from '../hooks' import OptionCard from './option-card' -import type { Datasource } from '../types' +import type { Datasource } from '../../types' type DataSourceOptionsProps = { dataSourceNodeId: string @@ -28,7 +28,6 @@ const DataSourceOptions = ({ useEffect(() => { if (options.length > 0 && !dataSourceNodeId) handelSelect(options[0].value) - // eslint-disable-next-line react-hooks/exhaustive-deps }, []) return ( diff --git a/web/app/components/rag-pipeline/components/panel/test-run/data-source-options/option-card.tsx b/web/app/components/rag-pipeline/components/panel/test-run/preparation/data-source-options/option-card.tsx similarity index 100% rename from web/app/components/rag-pipeline/components/panel/test-run/data-source-options/option-card.tsx rename to web/app/components/rag-pipeline/components/panel/test-run/preparation/data-source-options/option-card.tsx diff --git a/web/app/components/rag-pipeline/components/panel/test-run/document-processing/actions.tsx b/web/app/components/rag-pipeline/components/panel/test-run/preparation/document-processing/actions.tsx similarity index 100% rename from web/app/components/rag-pipeline/components/panel/test-run/document-processing/actions.tsx rename to web/app/components/rag-pipeline/components/panel/test-run/preparation/document-processing/actions.tsx diff --git a/web/app/components/rag-pipeline/components/panel/test-run/document-processing/hooks.ts b/web/app/components/rag-pipeline/components/panel/test-run/preparation/document-processing/hooks.ts similarity index 100% rename from web/app/components/rag-pipeline/components/panel/test-run/document-processing/hooks.ts rename to web/app/components/rag-pipeline/components/panel/test-run/preparation/document-processing/hooks.ts diff --git a/web/app/components/rag-pipeline/components/panel/test-run/document-processing/index.tsx b/web/app/components/rag-pipeline/components/panel/test-run/preparation/document-processing/index.tsx similarity index 100% rename from web/app/components/rag-pipeline/components/panel/test-run/document-processing/index.tsx rename to web/app/components/rag-pipeline/components/panel/test-run/preparation/document-processing/index.tsx diff --git a/web/app/components/rag-pipeline/components/panel/test-run/document-processing/options.tsx b/web/app/components/rag-pipeline/components/panel/test-run/preparation/document-processing/options.tsx similarity index 100% rename from web/app/components/rag-pipeline/components/panel/test-run/document-processing/options.tsx rename to web/app/components/rag-pipeline/components/panel/test-run/preparation/document-processing/options.tsx diff --git a/web/app/components/rag-pipeline/components/panel/test-run/footer-tips.tsx b/web/app/components/rag-pipeline/components/panel/test-run/preparation/footer-tips.tsx similarity index 100% rename from web/app/components/rag-pipeline/components/panel/test-run/footer-tips.tsx rename to web/app/components/rag-pipeline/components/panel/test-run/preparation/footer-tips.tsx diff --git a/web/app/components/rag-pipeline/components/panel/test-run/hooks.ts b/web/app/components/rag-pipeline/components/panel/test-run/preparation/hooks.ts similarity index 97% rename from web/app/components/rag-pipeline/components/panel/test-run/hooks.ts rename to web/app/components/rag-pipeline/components/panel/test-run/preparation/hooks.ts index a29ad759be..64f05c0cd5 100644 --- a/web/app/components/rag-pipeline/components/panel/test-run/hooks.ts +++ b/web/app/components/rag-pipeline/components/panel/test-run/preparation/hooks.ts @@ -1,6 +1,6 @@ import { useTranslation } from 'react-i18next' -import type { DataSourceOption } from './types' -import { TestRunStep } from './types' +import type { DataSourceOption } from '../types' +import { TestRunStep } from '../types' import { useNodes } from 'reactflow' import { BlockEnum } from '@/app/components/workflow/types' import type { DataSourceNodeType } from '@/app/components/workflow/nodes/data-source/types' diff --git a/web/app/components/rag-pipeline/components/panel/test-run/preparation/index.tsx b/web/app/components/rag-pipeline/components/panel/test-run/preparation/index.tsx new file mode 100644 index 0000000000..7dc5e760c9 --- /dev/null +++ b/web/app/components/rag-pipeline/components/panel/test-run/preparation/index.tsx @@ -0,0 +1,213 @@ +import React, { useCallback, useMemo, useState } from 'react' +import { + useOnlineDocument, + useOnlineDrive, + useTestRunSteps, + useWebsiteCrawl, +} from './hooks' +import DataSourceOptions from './data-source-options' +import LocalFile from '@/app/components/datasets/documents/create-from-pipeline/data-source/local-file' +import OnlineDocuments from '@/app/components/datasets/documents/create-from-pipeline/data-source/online-documents' +import WebsiteCrawl from '@/app/components/datasets/documents/create-from-pipeline/data-source/website-crawl' +import OnlineDrive from '@/app/components/datasets/documents/create-from-pipeline/data-source/online-drive' +import Actions from './actions' +import DocumentProcessing from './document-processing' +import { useWorkflowRun } from '@/app/components/workflow/hooks' +import type { Datasource } from '../types' +import { DatasourceType } from '@/models/pipeline' +import { TransferMethod } from '@/types/app' +import FooterTips from './footer-tips' +import { useDataSourceStore, useDataSourceStoreWithSelector } from '@/app/components/datasets/documents/create-from-pipeline/data-source/store' +import { useShallow } from 'zustand/react/shallow' +import { useWorkflowStore } from '@/app/components/workflow/store' +import StepIndicator from './step-indicator' + +const Preparation = () => { + const { + localFileList: fileList, + onlineDocuments, + websitePages, + selectedFileIds, + } = useDataSourceStoreWithSelector(useShallow(state => ({ + localFileList: state.localFileList, + onlineDocuments: state.onlineDocuments, + websitePages: state.websitePages, + selectedFileIds: state.selectedFileIds, + }))) + const workflowStore = useWorkflowStore() + const dataSourceStore = useDataSourceStore() + const [datasource, setDatasource] = useState() + + const { + steps, + currentStep, + handleNextStep, + handleBackStep, + } = useTestRunSteps() + + const { clearOnlineDocumentData } = useOnlineDocument() + const { clearWebsiteCrawlData } = useWebsiteCrawl() + const { clearOnlineDriveData } = useOnlineDrive() + + const datasourceType = datasource?.nodeData.provider_type + + const nextBtnDisabled = useMemo(() => { + if (!datasource) return true + if (datasourceType === DatasourceType.localFile) + return !fileList.length || fileList.some(file => !file.file.id) + if (datasourceType === DatasourceType.onlineDocument) + return !onlineDocuments.length + if (datasourceType === DatasourceType.websiteCrawl) + return !websitePages.length + if (datasourceType === DatasourceType.onlineDrive) + return !selectedFileIds.length + return false + }, [datasource, datasourceType, fileList, onlineDocuments.length, selectedFileIds.length, websitePages.length]) + + const { handleRun } = useWorkflowRun() + + const handleProcess = useCallback((data: Record) => { + if (!datasource) + return + const datasourceInfoList: Record[] = [] + const credentialId = dataSourceStore.getState().currentCredentialId + if (datasourceType === DatasourceType.localFile) { + const { localFileList } = dataSourceStore.getState() + const { id, name, type, size, extension, mime_type } = localFileList[0].file + const documentInfo = { + related_id: id, + name, + type, + size, + extension, + mime_type, + url: '', + transfer_method: TransferMethod.local_file, + } + datasourceInfoList.push(documentInfo) + } + if (datasourceType === DatasourceType.onlineDocument) { + const { onlineDocuments } = dataSourceStore.getState() + const { workspace_id, ...rest } = onlineDocuments[0] + const documentInfo = { + workspace_id, + page: rest, + credential_id: credentialId, + } + datasourceInfoList.push(documentInfo) + } + if (datasourceType === DatasourceType.websiteCrawl) { + const { websitePages } = dataSourceStore.getState() + datasourceInfoList.push({ + ...websitePages[0], + credential_id: credentialId, + }) + } + if (datasourceType === DatasourceType.onlineDrive) { + const { bucket, fileList, selectedFileIds } = dataSourceStore.getState() + const file = fileList.find(file => file.id === selectedFileIds[0]) + datasourceInfoList.push({ + bucket, + id: file?.id, + type: file?.type, + credential_id: credentialId, + }) + } + const { setIsPreparingDataSource } = workflowStore.getState() + handleRun({ + inputs: data, + start_node_id: datasource.nodeId, + datasource_type: datasourceType, + datasource_info_list: datasourceInfoList, + }) + setIsPreparingDataSource?.(false) + }, [dataSourceStore, datasource, datasourceType, handleRun, workflowStore]) + + const clearDataSourceData = useCallback((dataSource: Datasource) => { + if (dataSource.nodeData.provider_type === DatasourceType.onlineDocument) + clearOnlineDocumentData() + else if (dataSource.nodeData.provider_type === DatasourceType.websiteCrawl) + clearWebsiteCrawlData() + else if (dataSource.nodeData.provider_type === DatasourceType.onlineDrive) + clearOnlineDriveData() + }, []) + + const handleSwitchDataSource = useCallback((dataSource: Datasource) => { + const { + setCurrentCredentialId, + currentNodeIdRef, + } = dataSourceStore.getState() + clearDataSourceData(dataSource) + setCurrentCredentialId('') + currentNodeIdRef.current = dataSource.nodeId + setDatasource(dataSource) + }, [dataSourceStore]) + + const handleCredentialChange = useCallback((credentialId: string) => { + const { setCurrentCredentialId } = dataSourceStore.getState() + clearDataSourceData(datasource!) + setCurrentCredentialId(credentialId) + }, [dataSourceStore, datasource]) + return ( + <> + +
+ { + currentStep === 1 && ( + <> +
+ + {datasourceType === DatasourceType.localFile && ( + + )} + {datasourceType === DatasourceType.onlineDocument && ( + + )} + {datasourceType === DatasourceType.websiteCrawl && ( + + )} + {datasourceType === DatasourceType.onlineDrive && ( + + )} +
+ + + + ) + } + { + currentStep === 2 && ( + + ) + } +
+ + ) +} + +export default React.memo(Preparation) diff --git a/web/app/components/rag-pipeline/components/panel/test-run/step-indicator.tsx b/web/app/components/rag-pipeline/components/panel/test-run/preparation/step-indicator.tsx similarity index 87% rename from web/app/components/rag-pipeline/components/panel/test-run/step-indicator.tsx rename to web/app/components/rag-pipeline/components/panel/test-run/preparation/step-indicator.tsx index 5fc599833a..7227d98dc1 100644 --- a/web/app/components/rag-pipeline/components/panel/test-run/step-indicator.tsx +++ b/web/app/components/rag-pipeline/components/panel/test-run/preparation/step-indicator.tsx @@ -17,7 +17,7 @@ const StepIndicator = ({ steps, }: StepIndicatorProps) => { return ( -
+
{steps.map((step, index) => { const isCurrentStep = index === currentStep - 1 const isLastStep = index === steps.length - 1 @@ -26,7 +26,7 @@ const StepIndicator = ({
- {isCurrentStep &&
} + {isCurrentStep &&
} {step.label}
{!isLastStep && ( diff --git a/web/app/components/rag-pipeline/components/panel/test-run/result/index.tsx b/web/app/components/rag-pipeline/components/panel/test-run/result/index.tsx new file mode 100644 index 0000000000..5dfe4d9720 --- /dev/null +++ b/web/app/components/rag-pipeline/components/panel/test-run/result/index.tsx @@ -0,0 +1,71 @@ +import { + memo, + useState, +} from 'react' +import ResultPanel from '@/app/components/workflow/run/result-panel' +import TracingPanel from '@/app/components/workflow/run/tracing-panel' +import { useStore } from '@/app/components/workflow/store' +import { + WorkflowRunningStatus, +} from '@/app/components/workflow/types' +import Loading from '@/app/components/base/loading' +import Tabs from './tabs' +import ResultPreview from './result-preview' + +const Result = () => { + const workflowRunningData = useStore(s => s.workflowRunningData) + const [currentTab, setCurrentTab] = useState('RESULT') + + const switchTab = async (tab: string) => { + setCurrentTab(tab) + } + + return ( +
+ +
+ {currentTab === 'RESULT' && ( + switchTab('DETAIL')} + /> + )} + {currentTab === 'DETAIL' && ( + + )} + {currentTab === 'DETAIL' && !workflowRunningData?.result && ( +
+ +
+ )} + {currentTab === 'TRACING' && ( + + )} + {currentTab === 'TRACING' && !workflowRunningData?.tracing?.length && ( +
+ +
+ )} +
+
+ ) +} + +export default memo(Result) diff --git a/web/app/components/rag-pipeline/components/panel/test-run/result/result-preview/index.tsx b/web/app/components/rag-pipeline/components/panel/test-run/result/result-preview/index.tsx new file mode 100644 index 0000000000..aaf5a43d7c --- /dev/null +++ b/web/app/components/rag-pipeline/components/panel/test-run/result/result-preview/index.tsx @@ -0,0 +1,65 @@ +import Button from '@/app/components/base/button' +import { BlockEnum } from '@/app/components/workflow/types' +import type { NodeTracing } from '@/types/workflow' +import { RiLoader2Line } from '@remixicon/react' +import React, { useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import { ChunkCardList } from '../../../../chunk-card-list' + +type ResultTextProps = { + isRunning?: boolean + outputs?: any + error?: string + tracing?: NodeTracing[] + onSwitchToDetail: () => void +} + +const ResultPreview = ({ + isRunning, + outputs, + error, + tracing, + onSwitchToDetail, +}: ResultTextProps) => { + const { t } = useTranslation() + + const chunkInfo = useMemo(() => { + if (!outputs || !tracing) + return undefined + const knowledgeIndexNode = tracing.find(node => node.node_type === BlockEnum.KnowledgeBase) + return knowledgeIndexNode?.inputs?.chunks + }, [outputs, tracing]) + + return ( + <> + {isRunning && !outputs && ( +
+ +
{t('pipeline.result.resultPreview.loading')}
+
+ )} + {!isRunning && error && ( +
+
{t('pipeline.result.resultPreview.error')}
+ +
+ )} + {outputs && ( +
+ {!!chunkInfo && } +
+
+ + {t('pipeline.result.resultPreview.footerTip', { count: 20 })} + +
+
+
+ )} + + ) +} + +export default React.memo(ResultPreview) diff --git a/web/app/components/rag-pipeline/components/panel/test-run/result/tabs/index.tsx b/web/app/components/rag-pipeline/components/panel/test-run/result/tabs/index.tsx new file mode 100644 index 0000000000..b968446497 --- /dev/null +++ b/web/app/components/rag-pipeline/components/panel/test-run/result/tabs/index.tsx @@ -0,0 +1,45 @@ +import React from 'react' +import { useTranslation } from 'react-i18next' +import type { WorkflowRunningData } from '@/app/components/workflow/types' +import Tab from './tab' + +type TabsProps = { + currentTab: string + workflowRunningData?: WorkflowRunningData + switchTab: (tab: string) => void +} + +const Tabs = ({ + currentTab, + workflowRunningData, + switchTab, +}: TabsProps) => { + const { t } = useTranslation() + return ( +
+ + + +
+ ) +} + +export default React.memo(Tabs) diff --git a/web/app/components/rag-pipeline/components/panel/test-run/result/tabs/tab.tsx b/web/app/components/rag-pipeline/components/panel/test-run/result/tabs/tab.tsx new file mode 100644 index 0000000000..8c3e10b06e --- /dev/null +++ b/web/app/components/rag-pipeline/components/panel/test-run/result/tabs/tab.tsx @@ -0,0 +1,34 @@ +import React, { useCallback } from 'react' +import cn from '@/utils/classnames' +import type { WorkflowRunningData } from '@/app/components/workflow/types' + +type TabProps = { + isActive: boolean + label: string + value: string + workflowRunningData?: WorkflowRunningData + onClick: (value: string) => void +} + +const Tab = ({ isActive, label, value, workflowRunningData, onClick }: TabProps) => { + const handleClick = useCallback(() => { + onClick(value) + }, [value, onClick]) + + return ( + + ) +} + +export default React.memo(Tab) diff --git a/web/app/components/rag-pipeline/components/rag-pipeline-header/index.tsx b/web/app/components/rag-pipeline/components/rag-pipeline-header/index.tsx index 177778fcdb..74af61c603 100644 --- a/web/app/components/rag-pipeline/components/rag-pipeline-header/index.tsx +++ b/web/app/components/rag-pipeline/components/rag-pipeline-header/index.tsx @@ -1,6 +1,5 @@ import { memo, - useCallback, useMemo, } from 'react' import { useTranslation } from 'react-i18next' @@ -9,14 +8,13 @@ import Header from '@/app/components/workflow/header' import { fetchWorkflowRunHistory } from '@/service/workflow' import { useStore, - useWorkflowStore, } from '@/app/components/workflow/store' import InputFieldButton from './input-field-button' import Publisher from './publisher' +import RunMode from './run-mode' const RagPipelineHeader = () => { const { t } = useTranslation() - const workflowStore = useWorkflowStore() const pipelineId = useStore(s => s.pipelineId) const showDebugAndPreviewPanel = useStore(s => s.showDebugAndPreviewPanel) @@ -27,11 +25,6 @@ const RagPipelineHeader = () => { } }, [pipelineId]) - const handleStopRun = useCallback(() => { - const { setShowDebugAndPreviewPanel } = workflowStore.getState() - setShowDebugAndPreviewPanel(false) - }, [workflowStore]) - const headerProps: HeaderProps = useMemo(() => { return { normal: { @@ -41,17 +34,17 @@ const RagPipelineHeader = () => { }, runAndHistoryProps: { showRunButton: true, - runButtonText: t('workflow.singleRun.testRun'), viewHistoryProps, - isRunning: showDebugAndPreviewPanel, - onStopRun: handleStopRun, + components: { + RunMode, + }, }, }, viewHistory: { viewHistoryProps, }, } - }, [viewHistoryProps, showDebugAndPreviewPanel, handleStopRun, t]) + }, [viewHistoryProps, showDebugAndPreviewPanel, t]) return (
diff --git a/web/app/components/rag-pipeline/components/rag-pipeline-header/run-mode.tsx b/web/app/components/rag-pipeline/components/rag-pipeline-header/run-mode.tsx new file mode 100644 index 0000000000..304e21130a --- /dev/null +++ b/web/app/components/rag-pipeline/components/rag-pipeline-header/run-mode.tsx @@ -0,0 +1,117 @@ +import React, { useCallback } from 'react' +import { useTranslation } from 'react-i18next' +import { useWorkflowRun, useWorkflowStartRun } from '@/app/components/workflow/hooks' +import { useStore, useWorkflowStore } from '@/app/components/workflow/store' +import { WorkflowRunningStatus } from '@/app/components/workflow/types' +import { useEventEmitterContextContext } from '@/context/event-emitter' +import { EVENT_WORKFLOW_STOP } from '@/app/components/workflow/variable-inspect/types' +import { getKeyboardKeyNameBySystem } from '@/app/components/workflow/utils' +import cn from '@/utils/classnames' +import { RiCloseLine, RiDatabase2Line, RiLoader2Line, RiPlayLargeLine } from '@remixicon/react' +import { StopCircle } from '@/app/components/base/icons/src/vender/line/mediaAndDevices' + +type RunModeProps = { + text?: string +} + +const RunMode = ({ + text, +}: RunModeProps) => { + const { t } = useTranslation() + const { handleWorkflowStartRunInWorkflow } = useWorkflowStartRun() + const { handleStopRun } = useWorkflowRun() + const workflowStore = useWorkflowStore() + const workflowRunningData = useStore(s => s.workflowRunningData) + const isPreparingDataSource = useStore(s => s.isPreparingDataSource) + + const isRunning = workflowRunningData?.result.status === WorkflowRunningStatus.Running + const isDisabled = isPreparingDataSource || isRunning + + const handleStop = useCallback(() => { + handleStopRun(workflowRunningData?.task_id || '') + }, [handleStopRun, workflowRunningData?.task_id]) + + const handleCancelPreparingDataSource = useCallback(() => { + const { setIsPreparingDataSource, setShowDebugAndPreviewPanel } = workflowStore.getState() + setIsPreparingDataSource?.(false) + setShowDebugAndPreviewPanel(false) + }, [workflowStore]) + + const { eventEmitter } = useEventEmitterContextContext() + eventEmitter?.useSubscription((v: any) => { + if (v.type === EVENT_WORKFLOW_STOP) + handleStop() + }) + + return ( +
+ + {isRunning && ( + + )} + {isPreparingDataSource && ( + + )} +
+ ) +} + +export default React.memo(RunMode) diff --git a/web/app/components/rag-pipeline/hooks/use-pipeline-run.ts b/web/app/components/rag-pipeline/hooks/use-pipeline-run.ts index 4ed8cb95c7..11031b0a56 100644 --- a/web/app/components/rag-pipeline/hooks/use-pipeline-run.ts +++ b/web/app/components/rag-pipeline/hooks/use-pipeline-run.ts @@ -287,9 +287,10 @@ export const usePipelineRun = () => { ) const handleStopRun = useCallback((taskId: string) => { - const { pipelineId } = workflowStore.getState() + const { pipelineId, setShowDebugAndPreviewPanel } = workflowStore.getState() stopWorkflowRun(`/rag/pipeline/${pipelineId}/workflow-runs/tasks/${taskId}/stop`) + setShowDebugAndPreviewPanel(false) }, [workflowStore]) const handleRestoreFromPublishedWorkflow = useCallback((publishedWorkflow: VersionHistory) => { diff --git a/web/app/components/rag-pipeline/hooks/use-pipeline-start-run.tsx b/web/app/components/rag-pipeline/hooks/use-pipeline-start-run.tsx index e20101daef..9f9e6d0f2b 100644 --- a/web/app/components/rag-pipeline/hooks/use-pipeline-start-run.tsx +++ b/web/app/components/rag-pipeline/hooks/use-pipeline-start-run.tsx @@ -24,20 +24,32 @@ export const usePipelineStartRun = () => { return const { + isPreparingDataSource, + setIsPreparingDataSource, showDebugAndPreviewPanel, setShowEnvPanel, setShowDebugAndPreviewPanel, } = workflowStore.getState() + if (!isPreparingDataSource && workflowRunningData) { + workflowStore.setState({ + isPreparingDataSource: true, + workflowRunningData: undefined, + }) + return + } + setShowEnvPanel(false) closeAllInputFieldPanels() if (showDebugAndPreviewPanel) { + setIsPreparingDataSource?.(false) handleCancelDebugAndPreviewPanel() return } await doSyncWorkflowDraft() + setIsPreparingDataSource?.(true) setShowDebugAndPreviewPanel(true) }, [workflowStore, handleCancelDebugAndPreviewPanel, doSyncWorkflowDraft]) diff --git a/web/app/components/rag-pipeline/store/index.ts b/web/app/components/rag-pipeline/store/index.ts index fd75719bcd..0e7f46fa4b 100644 --- a/web/app/components/rag-pipeline/store/index.ts +++ b/web/app/components/rag-pipeline/store/index.ts @@ -24,6 +24,8 @@ export type RagPipelineSliceShape = { setRagPipelineVariables: (ragPipelineVariables: RAGPipelineVariables) => void dataSourceList: ToolWithProvider[] setDataSourceList: (dataSourceList: DataSourceItem[]) => void + isPreparingDataSource: boolean + setIsPreparingDataSource: (isPreparingDataSource: boolean) => void } export type CreateRagPipelineSliceSlice = StateCreator @@ -45,4 +47,6 @@ export const createRagPipelineSliceSlice: StateCreator = const formattedDataSourceList = dataSourceList.map(item => transformDataSourceToTool(item)) set(() => ({ dataSourceList: formattedDataSourceList })) }, + isPreparingDataSource: false, + setIsPreparingDataSource: isPreparingDataSource => set(() => ({ isPreparingDataSource })), }) diff --git a/web/app/components/workflow/header/run-and-history.tsx b/web/app/components/workflow/header/run-and-history.tsx index 41b797d346..101167408e 100644 --- a/web/app/components/workflow/header/run-and-history.tsx +++ b/web/app/components/workflow/header/run-and-history.tsx @@ -1,97 +1,17 @@ import { memo } from 'react' import { useTranslation } from 'react-i18next' import { - RiLoader2Line, RiPlayLargeLine, } from '@remixicon/react' -import { useStore } from '../store' import { useNodesReadOnly, - useWorkflowRun, useWorkflowStartRun, } from '../hooks' -import { WorkflowRunningStatus } from '../types' import type { ViewHistoryProps } from './view-history' import ViewHistory from './view-history' import Checklist from './checklist' import cn from '@/utils/classnames' -import { - StopCircle, -} from '@/app/components/base/icons/src/vender/line/mediaAndDevices' -import { useEventEmitterContextContext } from '@/context/event-emitter' -import { EVENT_WORKFLOW_STOP } from '@/app/components/workflow/variable-inspect/types' - -type RunModeProps = { - text?: string - isRunning?: boolean - onStopRun?: () => void -} -const RunMode = memo(({ - text, - isRunning: running, - onStopRun, -}: RunModeProps) => { - const { t } = useTranslation() - const { handleWorkflowStartRunInWorkflow } = useWorkflowStartRun() - const { handleStopRun } = useWorkflowRun() - const workflowRunningData = useStore(s => s.workflowRunningData) - const isRunning = workflowRunningData?.result.status === WorkflowRunningStatus.Running - const mergedRunning = isRunning || running - - const handleStop = () => { - handleStopRun(workflowRunningData?.task_id || '') - } - - const { eventEmitter } = useEventEmitterContextContext() - eventEmitter?.useSubscription((v: any) => { - if (v.type === EVENT_WORKFLOW_STOP) - handleStop() - }) - - return ( - <> -
{ - handleWorkflowStartRunInWorkflow() - }} - > - { - mergedRunning - ? ( - <> - - {t('workflow.common.running')} - - ) - : ( - <> - - {text ?? t('workflow.common.run')} - - ) - } -
- { - mergedRunning && ( -
onStopRun ? onStopRun() : handleStopRun(workflowRunningData?.task_id || '')} - > - -
- ) - } - - ) -}) +import RunMode from './run-mode' const PreviewMode = memo(() => { const { t } = useTranslation() @@ -115,24 +35,32 @@ export type RunAndHistoryProps = { showRunButton?: boolean runButtonText?: string isRunning?: boolean - onStopRun?: () => void showPreviewButton?: boolean viewHistoryProps?: ViewHistoryProps + components?: { + RunMode?: React.ComponentType< + { + text?: string + } + > + } } const RunAndHistory = ({ showRunButton, runButtonText, - isRunning, - onStopRun, showPreviewButton, viewHistoryProps, + components, }: RunAndHistoryProps) => { const { nodesReadOnly } = useNodesReadOnly() + const { RunMode: CustomRunMode } = components || {} return (
{ - showRunButton && + showRunButton && ( + CustomRunMode ? : + ) } { showPreviewButton && diff --git a/web/app/components/workflow/header/run-mode.tsx b/web/app/components/workflow/header/run-mode.tsx new file mode 100644 index 0000000000..d1fd3510cc --- /dev/null +++ b/web/app/components/workflow/header/run-mode.tsx @@ -0,0 +1,96 @@ +import React, { useCallback } from 'react' +import { useTranslation } from 'react-i18next' +import { useWorkflowRun, useWorkflowStartRun } from '@/app/components/workflow/hooks' +import { useStore } from '@/app/components/workflow/store' +import { WorkflowRunningStatus } from '@/app/components/workflow/types' +import { useEventEmitterContextContext } from '@/context/event-emitter' +import { EVENT_WORKFLOW_STOP } from '@/app/components/workflow/variable-inspect/types' +import { getKeyboardKeyNameBySystem } from '@/app/components/workflow/utils' +import cn from '@/utils/classnames' +import { RiLoader2Line, RiPlayLargeLine } from '@remixicon/react' +import { StopCircle } from '@/app/components/base/icons/src/vender/line/mediaAndDevices' + +type RunModeProps = { + text?: string +} + +const RunMode = ({ + text, +}: RunModeProps) => { + const { t } = useTranslation() + const { handleWorkflowStartRunInWorkflow } = useWorkflowStartRun() + const { handleStopRun } = useWorkflowRun() + const workflowRunningData = useStore(s => s.workflowRunningData) + + const isRunning = workflowRunningData?.result.status === WorkflowRunningStatus.Running + + const handleStop = useCallback(() => { + handleStopRun(workflowRunningData?.task_id || '') + }, [handleStopRun, workflowRunningData?.task_id]) + + const { eventEmitter } = useEventEmitterContextContext() + eventEmitter?.useSubscription((v: any) => { + if (v.type === EVENT_WORKFLOW_STOP) + handleStop() + }) + + return ( +
+ + { + isRunning && ( + + ) + } +
+ ) +} + +export default React.memo(RunMode) diff --git a/web/app/components/workflow/variable-inspect/value-content.tsx b/web/app/components/workflow/variable-inspect/value-content.tsx index 87673cea6f..fef17efc11 100644 --- a/web/app/components/workflow/variable-inspect/value-content.tsx +++ b/web/app/components/workflow/variable-inspect/value-content.tsx @@ -72,7 +72,7 @@ const DisplayContent = (props: DisplayContentProps) => { btnClassName='!pl-1.5 !pr-0.5 gap-[3px]' />
-
+
{viewMode === ViewMode.Code && ( type === ContentType.Markdown ?