refactor(test-run, preparation): restructure test run components, enhance data handling, and improve user experience with new loading states and error handling

This commit is contained in:
twwu 2025-08-20 16:40:56 +08:00
parent 8ab3f1212b
commit 449755ada4
34 changed files with 889 additions and 479 deletions

View File

@ -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(() => {

View File

@ -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<string, any>) => {
if (!datasource)
return
const { bucket, currentCredentialId, fileList: onlineDriveFileList } = dataSourceStore.getState()
const { currentCredentialId } = dataSourceStore.getState()
const datasourceInfoList: Record<string, any>[] = []
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

View File

@ -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 <div className="inline-flex items-start justify-start gap-1 self-stretch">
<div className="w-4 text-[13px] font-medium leading-5 text-text-tertiary">{type === QAItemType.Question ? 'Q' : 'A'}</div>
<div className="body-md-regular flex-1 text-text-secondary">{text}</div>
</div>
const { type, text } = props
return <div className='inline-flex items-start justify-start gap-1 self-stretch'>
<div className='w-4 text-[13px] font-medium leading-5 text-text-tertiary'>{type === QAItemType.Question ? 'Q' : 'A'}</div>
<div className='body-md-regular flex-1 text-text-secondary'>{text}</div>
</div>
}
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 (
<PreviewSlice
key={child}
label={`C-${indexForLabel}`}
text={child}
tooltip={`Child-chunk-${indexForLabel} · ${child.length} Characters`}
labelInnerClassName='text-[10px] font-semibold align-bottom leading-7'
dividerClassName='leading-7'
/>
)
})
}
// ChunkType.QA
if (typeof content === 'object') {
return <div className="flex flex-col gap-2">
<QAItem type={QAItemType.Question} text={(content as QAChunk).question} />
<QAItem type={QAItemType.Answer} text={(content as QAChunk).answer} />
</div>
}
// ChunkType.General
return content
const renderContent = () => {
// ChunkType.Paragraph && ChunkType.FullDoc
if (Array.isArray(content)) {
return content.map((child, index) => {
const indexForLabel = index + 1
return (
<PreviewSlice
key={child}
label={`C-${indexForLabel}`}
text={child}
tooltip={`Child-chunk-${indexForLabel} · ${child.length} Characters`}
labelInnerClassName='text-[10px] font-semibold align-bottom leading-7'
dividerClassName='leading-7'
/>
)
})
}
return <div className="inline-flex flex-col gap-1 self-stretch rounded-lg bg-components-panel-bg px-3 py-2.5">
{type !== ChunkType.FullDoc && <div className="inline-flex items-center justify-start gap-2">
<SegmentIndexTag
positionId={positionId}
labelPrefix={type === ChunkType.Paragraph ? 'Parent-Chunk' : 'Chunk'}
/>
<Dot />
<div className='system-xs-medium text-text-tertiary'>{formatNumber(wordCount)} {t('datasetDocuments.segment.characters', { count: wordCount })}</div>
</div>}
<div className="body-md-regular text-text-secondary">{renderContent()}</div>
// ChunkType.QA
if (typeof content === 'object') {
return <div className='flex flex-col gap-2'>
<QAItem type={QAItemType.Question} text={(content as QAChunk).question} />
<QAItem type={QAItemType.Answer} text={(content as QAChunk).answer} />
</div>
}
// ChunkType.General
return content
}
return (
<div className='flex flex-col gap-1 rounded-lg bg-components-panel-bg px-3 py-2.5'>
{type !== ChunkType.FullDoc && <div className='inline-flex items-center justify-start gap-2'>
<SegmentIndexTag
positionId={positionId}
labelPrefix={type === ChunkType.Paragraph ? 'Parent-Chunk' : 'Chunk'}
/>
<Dot />
<div className='system-xs-medium text-text-tertiary'>{formatNumber(wordCount)} {t('datasetDocuments.segment.characters', { count: wordCount })}</div>
</div>}
<div className='body-md-regular text-text-secondary'>{renderContent()}</div>
</div>
)
}
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 <div className='flex grow flex-col gap-1'>
{(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 <ChunkCard
type={chunkType}
content={isParentChildMode ? (seg as ParentChildChunk).child_contents : (seg as string | QAChunk)}
wordCount={wordCount}
positionId={index + 1}
/>
})}
return (
<div className={cn('flex w-full flex-col gap-y-1', className)}>
{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 (
<ChunkCard
type={chunkType}
content={isParentChildMode ? (seg as ParentChildChunk).child_contents : (seg as string | QAChunk)}
wordCount={wordCount}
positionId={index + 1}
/>
)
})}
</div>
)
}

View File

@ -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'

View File

@ -1,22 +0,0 @@
import React from 'react'
import { RiCloseLine } from '@remixicon/react'
type CloseButtonProps = {
handleClose: () => void
}
const CloseButton = ({
handleClose,
}: CloseButtonProps) => {
return (
<button
type='button'
className='absolute right-2.5 top-2.5 flex size-8 items-center justify-center p-1.5'
onClick={handleClose}
>
<RiCloseLine className='size-4 text-text-tertiary' />
</button>
)
}
export default React.memo(CloseButton)

View File

@ -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 (
<div className='flex flex-col gap-y-0.5 px-3 pb-2 pt-3.5'>
<div className='system-md-semibold-uppercase flex items-center gap-x-1 pl-1 pr-8 text-text-primary'>
<div className='flex items-center gap-x-2 pl-4 pr-3 pt-4'>
<div className='system-xl-semibold grow pl-1 pr-8 text-text-primary'>
{t('datasetPipeline.testRun.title')}
</div>
<StepIndicator steps={steps} currentStep={currentStep} />
<button
type='button'
className='flex size-8 shrink-0 items-center justify-center p-1.5'
onClick={handleClose}
>
<RiCloseLine className='size-4 text-text-tertiary' />
</button>
</div>
)
}

View File

@ -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<Datasource>()
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<string, any>) => {
if (!datasource)
return
const datasourceInfoList: Record<string, any>[] = []
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 (
<div
className='relative flex h-full w-[480px] flex-col rounded-l-2xl border-y-[0.5px] border-l-[0.5px] border-components-panel-border bg-components-panel-bg shadow-xl shadow-shadow-shadow-1'
>
<CloseButton handleClose={handleClose} />
<Header steps={steps} currentStep={currentStep} />
<div className='flex grow flex-col overflow-y-auto'>
{
currentStep === 1 && (
<>
<div className='flex flex-col gap-y-4 px-4 py-2'>
<DataSourceOptions
dataSourceNodeId={datasource?.nodeId || ''}
onSelect={handleSwitchDataSource}
/>
{datasourceType === DatasourceType.localFile && (
<LocalFile
allowedExtensions={datasource!.nodeData.fileExtensions || []}
notSupportBatchUpload={false} // only support single file upload in test run
/>
)}
{datasourceType === DatasourceType.onlineDocument && (
<OnlineDocuments
nodeId={datasource!.nodeId}
nodeData={datasource!.nodeData}
isInPipeline
onCredentialChange={handleCredentialChange}
/>
)}
{datasourceType === DatasourceType.websiteCrawl && (
<WebsiteCrawl
nodeId={datasource!.nodeId}
nodeData={datasource!.nodeData}
isInPipeline
onCredentialChange={handleCredentialChange}
/>
)}
{datasourceType === DatasourceType.onlineDrive && (
<OnlineDrive
nodeId={datasource!.nodeId}
nodeData={datasource!.nodeData}
isInPipeline
onCredentialChange={handleCredentialChange}
/>
)}
</div>
<Actions disabled={nextBtnDisabled} handleNextStep={handleNextStep} />
<FooterTips />
</>
)
}
{
currentStep === 2 && (
<DocumentProcessing
dataSourceNodeId={datasource!.nodeId}
onProcess={handleProcess}
onBack={handleBackStep}
/>
)
}
</div>
<Header />
{isPreparingDataSource ? (
<DataSourceProvider>
<Preparation />
</DataSourceProvider>
) : (
<Result />
)}
</div>
)
}
const TestRunPanelWrapper = () => {
return (
<DataSourceProvider>
<TestRunPanel />
</DataSourceProvider>
)
}
export default TestRunPanelWrapper
export default TestRunPanel

View File

@ -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 (

View File

@ -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'

View File

@ -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<Datasource>()
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<string, any>) => {
if (!datasource)
return
const datasourceInfoList: Record<string, any>[] = []
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 (
<>
<StepIndicator steps={steps} currentStep={currentStep} />
<div className='flex grow flex-col overflow-y-auto'>
{
currentStep === 1 && (
<>
<div className='flex flex-col gap-y-4 px-4 py-2'>
<DataSourceOptions
dataSourceNodeId={datasource?.nodeId || ''}
onSelect={handleSwitchDataSource}
/>
{datasourceType === DatasourceType.localFile && (
<LocalFile
allowedExtensions={datasource!.nodeData.fileExtensions || []}
notSupportBatchUpload={false} // only support single file upload in test run
/>
)}
{datasourceType === DatasourceType.onlineDocument && (
<OnlineDocuments
nodeId={datasource!.nodeId}
nodeData={datasource!.nodeData}
isInPipeline
onCredentialChange={handleCredentialChange}
/>
)}
{datasourceType === DatasourceType.websiteCrawl && (
<WebsiteCrawl
nodeId={datasource!.nodeId}
nodeData={datasource!.nodeData}
isInPipeline
onCredentialChange={handleCredentialChange}
/>
)}
{datasourceType === DatasourceType.onlineDrive && (
<OnlineDrive
nodeId={datasource!.nodeId}
nodeData={datasource!.nodeData}
isInPipeline
onCredentialChange={handleCredentialChange}
/>
)}
</div>
<Actions disabled={nextBtnDisabled} handleNextStep={handleNextStep} />
<FooterTips />
</>
)
}
{
currentStep === 2 && (
<DocumentProcessing
dataSourceNodeId={datasource!.nodeId}
onProcess={handleProcess}
onBack={handleBackStep}
/>
)
}
</div>
</>
)
}
export default React.memo(Preparation)

View File

@ -17,7 +17,7 @@ const StepIndicator = ({
steps,
}: StepIndicatorProps) => {
return (
<div className='flex items-center gap-x-2 px-1'>
<div className='flex items-center gap-x-2 px-4 pb-2'>
{steps.map((step, index) => {
const isCurrentStep = index === currentStep - 1
const isLastStep = index === steps.length - 1
@ -26,7 +26,7 @@ const StepIndicator = ({
<div
className={cn('flex items-center gap-x-1', isCurrentStep ? 'text-state-accent-solid' : 'text-text-tertiary')}
>
{isCurrentStep && <div className='size-1 rounded-full bg-state-accent-solid' />}
{isCurrentStep && <div className='size-1 rounded-full bg-state-accent-solid' />}
<span className='system-2xs-semibold-uppercase'>{step.label}</span>
</div>
{!isLastStep && (

View File

@ -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<string>('RESULT')
const switchTab = async (tab: string) => {
setCurrentTab(tab)
}
return (
<div className='flex grow flex-col'>
<Tabs currentTab={currentTab} workflowRunningData={workflowRunningData} switchTab={switchTab} />
<div className='flex h-0 grow flex-col overflow-y-auto'>
{currentTab === 'RESULT' && (
<ResultPreview
isRunning={!workflowRunningData?.result || workflowRunningData?.result.status === WorkflowRunningStatus.Running}
outputs={workflowRunningData?.result?.outputs}
error={workflowRunningData?.result?.error}
tracing={workflowRunningData?.tracing}
onSwitchToDetail={() => switchTab('DETAIL')}
/>
)}
{currentTab === 'DETAIL' && (
<ResultPanel
inputs={workflowRunningData?.result?.inputs}
outputs={workflowRunningData?.result?.outputs}
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 as any)?.name}
steps={workflowRunningData?.result?.total_steps}
exceptionCounts={workflowRunningData?.result?.exceptions_count}
/>
)}
{currentTab === 'DETAIL' && !workflowRunningData?.result && (
<div className='flex h-full items-center justify-center'>
<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'>
<Loading />
</div>
)}
</div>
</div>
)
}
export default memo(Result)

View File

@ -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 && (
<div className='flex grow flex-col items-center justify-center gap-y-2 pb-20'>
<RiLoader2Line className='size-4 animate-spin' />
<div className='system-sm-regular text-text-tertiary'>{t('pipeline.result.resultPreview.loading')}</div>
</div>
)}
{!isRunning && error && (
<div className='flex grow flex-col items-center justify-center gap-y-2 pb-20'>
<div className='system-sm-regular text-text-tertiary'>{t('pipeline.result.resultPreview.error')}</div>
<Button onClick={onSwitchToDetail}>
{t('pipeline.result.resultPreview.viewDetails')}
</Button>
</div>
)}
{outputs && (
<div className='flex grow flex-col bg-background-body p-1'>
{!!chunkInfo && <ChunkCardList chunkInfo={chunkInfo} />}
<div className='system-xs-regular mt-1 flex items-center gap-x-2 text-text-tertiary'>
<div className='h-px flex-1 bg-gradient-to-r from-background-gradient-mask-transparent to-divider-regular' />
<span className='shrink-0truncate' title={t('pipeline.result.resultPreview.footerTip', { count: 20 })}>
{t('pipeline.result.resultPreview.footerTip', { count: 20 })}
</span>
<div className='h-px flex-1 bg-gradient-to-l from-background-gradient-mask-transparent to-divider-regular' />
</div>
</div>
)}
</>
)
}
export default React.memo(ResultPreview)

View File

@ -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 (
<div className='flex shrink-0 items-center gap-x-6 border-b-[0.5px] border-divider-subtle px-4'>
<Tab
isActive={currentTab === 'RESULT'}
label={t('runLog.result')}
value='RESULT'
workflowRunningData={workflowRunningData}
onClick={switchTab}
/>
<Tab
isActive={currentTab === 'DETAIL'}
label={t('runLog.detail')}
value='DETAIL'
workflowRunningData={workflowRunningData}
onClick={switchTab}
/>
<Tab
isActive={currentTab === 'TRACING'}
label={t('runLog.tracing')}
value='TRACING'
workflowRunningData={workflowRunningData}
onClick={switchTab}
/>
</div>
)
}
export default React.memo(Tabs)

View File

@ -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 (
<button
type='button'
className={cn(
'system-sm-semibold-uppercase cursor-pointer border-b-2 border-transparent py-3 text-text-tertiary',
isActive && 'border-util-colors-blue-brand-blue-brand-600 text-text-primary',
!workflowRunningData && '!cursor-not-allowed opacity-30',
)}
onClick={handleClick}
disabled={!workflowRunningData}
>
{label}
</button>
)
}
export default React.memo(Tab)

View File

@ -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 (
<Header {...headerProps} />

View File

@ -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 (
<div className='flex items-center gap-x-px'>
<button
type='button'
className={cn(
'system-xs-medium flex h-7 items-center gap-x-1 px-1.5 text-text-accent hover:bg-state-accent-hover',
isDisabled && 'cursor-not-allowed bg-state-accent-hover',
isDisabled ? 'rounded-l-md' : 'rounded-md',
)}
onClick={() => {
handleWorkflowStartRunInWorkflow()
}}
disabled={isDisabled}
>
{!isDisabled && (
<>
<RiPlayLargeLine className='mr-1 size-4' />
{workflowRunningData ? t('pipeline.common.reRun') : (text ?? t('pipeline.common.testRun'))}
</>
)}
{isRunning && (
<>
<RiLoader2Line className='mr-1 size-4 animate-spin' />
{t('pipeline.common.processing')}
</>
)}
{isPreparingDataSource && (
<>
<RiDatabase2Line className='mr-1 size-4' />
{t('pipeline.common.preparingDataSource')}
</>
)}
{
!isDisabled && (
<div className='system-kbd flex items-center gap-x-0.5 text-text-tertiary'>
<div className='flex size-4 items-center justify-center rounded-[4px] bg-components-kbd-bg-gray'>
{getKeyboardKeyNameBySystem('alt')}
</div>
<div className='flex size-4 items-center justify-center rounded-[4px] bg-components-kbd-bg-gray'>
R
</div>
</div>
)
}
</button>
{isRunning && (
<button
type='button'
className={cn(
'flex size-7 items-center justify-center rounded-r-md bg-state-accent-active',
)}
onClick={handleStop}
>
<StopCircle className='size-4 text-text-accent' />
</button>
)}
{isPreparingDataSource && (
<button
type='button'
className={cn(
'flex size-7 items-center justify-center rounded-r-md bg-state-accent-active',
)}
onClick={handleCancelPreparingDataSource}
>
<RiCloseLine className='size-4 text-text-accent' />
</button>
)}
</div>
)
}
export default React.memo(RunMode)

View File

@ -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) => {

View File

@ -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])

View File

@ -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<RagPipelineSliceShape>
@ -45,4 +47,6 @@ export const createRagPipelineSliceSlice: StateCreator<RagPipelineSliceShape> =
const formattedDataSourceList = dataSourceList.map(item => transformDataSourceToTool(item))
set(() => ({ dataSourceList: formattedDataSourceList }))
},
isPreparingDataSource: false,
setIsPreparingDataSource: isPreparingDataSource => set(() => ({ isPreparingDataSource })),
})

View File

@ -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 (
<>
<div
className={cn(
'flex h-7 items-center px-2.5 text-[13px] font-medium text-components-button-secondary-accent-text',
'cursor-pointer hover:bg-state-accent-hover',
mergedRunning && 'cursor-not-allowed bg-state-accent-hover',
mergedRunning ? 'rounded-l-md' : 'rounded-md',
)}
onClick={() => {
handleWorkflowStartRunInWorkflow()
}}
>
{
mergedRunning
? (
<>
<RiLoader2Line className='mr-1 h-4 w-4 animate-spin' />
{t('workflow.common.running')}
</>
)
: (
<>
<RiPlayLargeLine className='mr-1 h-4 w-4' />
{text ?? t('workflow.common.run')}
</>
)
}
</div>
{
mergedRunning && (
<div
className={cn(
'ml-[1px] flex h-7 w-7 cursor-pointer items-center justify-center rounded-r-md bg-state-accent-active',
)}
onClick={() => onStopRun ? onStopRun() : handleStopRun(workflowRunningData?.task_id || '')}
>
<StopCircle className='h-4 w-4 text-text-accent' />
</div>
)
}
</>
)
})
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 (
<div className='flex h-8 items-center rounded-lg border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg px-0.5 shadow-xs'>
{
showRunButton && <RunMode text={runButtonText} isRunning={isRunning} onStopRun={onStopRun} />
showRunButton && (
CustomRunMode ? <CustomRunMode text={runButtonText} /> : <RunMode text={runButtonText} />
)
}
{
showPreviewButton && <PreviewMode />

View File

@ -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 (
<div className='flex items-center gap-x-px'>
<button
type='button'
className={cn(
'system-xs-medium flex h-7 items-center gap-x-1 px-1.5 text-text-accent hover:bg-state-accent-hover',
isRunning && 'cursor-not-allowed bg-state-accent-hover',
isRunning ? 'rounded-l-md' : 'rounded-md',
)}
onClick={() => {
handleWorkflowStartRunInWorkflow()
}}
disabled={isRunning}
>
{
isRunning
? (
<>
<RiLoader2Line className='mr-1 size-4 animate-spin' />
{t('workflow.common.running')}
</>
)
: (
<>
<RiPlayLargeLine className='mr-1 size-4' />
{text ?? t('workflow.common.run')}
</>
)
}
{
!isRunning && (
<div className='system-kbd flex items-center gap-x-0.5 text-text-tertiary'>
<div className='flex size-4 items-center justify-center rounded-[4px] bg-components-kbd-bg-gray'>
{getKeyboardKeyNameBySystem('alt')}
</div>
<div className='flex size-4 items-center justify-center rounded-[4px] bg-components-kbd-bg-gray'>
R
</div>
</div>
)
}
</button>
{
isRunning && (
<button
type='button'
className={cn(
'flex size-7 items-center justify-center rounded-r-md bg-state-accent-active',
)}
onClick={handleStop}
>
<StopCircle className='size-4 text-text-accent' />
</button>
)
}
</div>
)
}
export default React.memo(RunMode)

View File

@ -72,7 +72,7 @@ const DisplayContent = (props: DisplayContentProps) => {
btnClassName='!pl-1.5 !pr-0.5 gap-[3px]'
/>
</div>
<div className='flex flex-1 overflow-auto rounded-b-[10px] pb-1 pl-3 pr-1'>
<div className='flex flex-1 overflow-auto rounded-b-[10px] pl-3 pr-1'>
{viewMode === ViewMode.Code && (
type === ContentType.Markdown
? <Textarea

View File

@ -10,6 +10,10 @@ const translation = {
description: 'Knowledge description',
descriptionPlaceholder: 'Please enter the description of this Knowledge Pipeline. (Optional) ',
},
testRun: 'Test Run',
preparingDataSource: 'Preparing Data Source',
reRun: 'Re-run',
processing: 'Processing',
},
inputField: {
create: 'Create user input field',
@ -19,6 +23,14 @@ const translation = {
title: 'This pipeline has not yet been published',
desc: 'When the pipeline is not published, you can modify the chunk structure in the knowledge base node, and the pipeline orchestration and changes will be automatically saved as a draft.',
},
result: {
resultPreview: {
loading: 'Processing...Please wait',
error: 'Error occurred during execution',
viewDetails: 'View details',
footerTip: 'In test run mode, preview up to {{count}} chunks',
},
},
}
export default translation

View File

@ -212,8 +212,10 @@ const translation = {
toolParameterRequired: '{{field}}: parameter [{{param}}] is required',
},
singleRun: {
testRun: 'Test Run ',
testRun: 'Test Run',
startRun: 'Start Run',
preparingDataSource: 'Preparing Data Source',
reRun: 'Re-run',
running: 'Running',
testRunIteration: 'Test Run Iteration',
back: 'Back',
@ -974,7 +976,7 @@ const translation = {
updateSuccess: 'Version updated',
updateFailure: 'Failed to update version',
copyIdSuccess: 'ID copied to clipboard',
},
},
},
debug: {
settingsTab: 'Settings',

View File

@ -10,6 +10,10 @@ const translation = {
description: '知识流水线描述',
descriptionPlaceholder: '请输入此知识流水线的描述。 (可选)',
},
testRun: '测试运行',
preparingDataSource: '准备数据源',
reRun: '重新运行',
processing: '处理中',
},
inputField: {
create: '创建用户输入字段',
@ -19,6 +23,14 @@ const translation = {
title: '此知识流水线尚未发布',
desc: '当知识流水线未发布时,您可以修改知识库节点中的分块结构,知识流水线编排和更改将自动保存为草稿。',
},
result: {
resultPreview: {
loading: '处理中...请稍后',
error: '执行过程中出现错误',
viewDetails: '查看详情',
footerTip: '在测试运行模式下,最多预览 {{count}} 个分段',
},
},
}
export default translation

View File

@ -212,8 +212,10 @@ const translation = {
toolParameterRequired: '{{field}}: 参数 [{{param}}] 不能为空',
},
singleRun: {
testRun: '测试运行 ',
testRun: '测试运行',
startRun: '开始运行',
preparingDataSource: '准备数据源',
reRun: '重新运行',
running: '运行中',
testRunIteration: '测试运行迭代',
back: '返回',
@ -937,20 +939,6 @@ const translation = {
indexMethodIsRequired: '索引方法是必需的',
retrievalSettingIsRequired: '检索设置是必需的',
},
dataSource: {
supportedFileFormats: '支持的文件格式',
supportedFileFormatsPlaceholder: '文件格式例如doc',
add: '添加数据源',
},
knowledgeBase: {
chunkStructure: '分段结构',
chooseChunkStructure: '选择分段结构',
changeChunkStructure: '更改分段结构',
aboutRetrieval: '关于知识检索。',
chunkIsRequired: '分段结构是必需的',
indexMethodIsRequired: '索引方法是必需的',
retrievalSettingIsRequired: '检索设置是必需的',
},
},
tracing: {
stopBy: '由{{user}}终止',