mirror of
https://github.com/langgenius/dify.git
synced 2026-05-03 07:46:31 +08:00
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:
parent
8ab3f1212b
commit
449755ada4
@ -196,7 +196,7 @@ export const useOnlineDrive = () => {
|
|||||||
const dataSourceStore = useDataSourceStore()
|
const dataSourceStore = useDataSourceStore()
|
||||||
|
|
||||||
const selectedOnlineDriveFileList = useMemo(() => {
|
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])
|
}, [fileList, selectedFileIds])
|
||||||
|
|
||||||
const clearOnlineDriveData = useCallback(() => {
|
const clearOnlineDriveData = useCallback(() => {
|
||||||
|
|||||||
@ -87,7 +87,7 @@ const CreateFormPipeline = () => {
|
|||||||
clearOnlineDriveData,
|
clearOnlineDriveData,
|
||||||
} = useOnlineDrive()
|
} = useOnlineDrive()
|
||||||
|
|
||||||
const datasourceType = datasource?.nodeData.provider_type
|
const datasourceType = useMemo(() => datasource?.nodeData.provider_type, [datasource])
|
||||||
const isVectorSpaceFull = plan.usage.vectorSpace >= plan.total.vectorSpace
|
const isVectorSpaceFull = plan.usage.vectorSpace >= plan.total.vectorSpace
|
||||||
const isShowVectorSpaceFull = useMemo(() => {
|
const isShowVectorSpaceFull = useMemo(() => {
|
||||||
if (!datasource)
|
if (!datasource)
|
||||||
@ -234,10 +234,13 @@ const CreateFormPipeline = () => {
|
|||||||
const handleProcess = useCallback(async (data: Record<string, any>) => {
|
const handleProcess = useCallback(async (data: Record<string, any>) => {
|
||||||
if (!datasource)
|
if (!datasource)
|
||||||
return
|
return
|
||||||
const { bucket, currentCredentialId, fileList: onlineDriveFileList } = dataSourceStore.getState()
|
const { currentCredentialId } = dataSourceStore.getState()
|
||||||
const datasourceInfoList: Record<string, any>[] = []
|
const datasourceInfoList: Record<string, any>[] = []
|
||||||
if (datasourceType === DatasourceType.localFile) {
|
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 { id, name, type, size, extension, mime_type } = file.file
|
||||||
const documentInfo = {
|
const documentInfo = {
|
||||||
related_id: id,
|
related_id: id,
|
||||||
@ -254,6 +257,9 @@ const CreateFormPipeline = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (datasourceType === DatasourceType.onlineDocument) {
|
if (datasourceType === DatasourceType.onlineDocument) {
|
||||||
|
const {
|
||||||
|
onlineDocuments,
|
||||||
|
} = dataSourceStore.getState()
|
||||||
onlineDocuments.forEach((page) => {
|
onlineDocuments.forEach((page) => {
|
||||||
const { workspace_id, ...rest } = page
|
const { workspace_id, ...rest } = page
|
||||||
const documentInfo = {
|
const documentInfo = {
|
||||||
@ -265,6 +271,9 @@ const CreateFormPipeline = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (datasourceType === DatasourceType.websiteCrawl) {
|
if (datasourceType === DatasourceType.websiteCrawl) {
|
||||||
|
const {
|
||||||
|
websitePages,
|
||||||
|
} = dataSourceStore.getState()
|
||||||
websitePages.forEach((websitePage) => {
|
websitePages.forEach((websitePage) => {
|
||||||
datasourceInfoList.push({
|
datasourceInfoList.push({
|
||||||
...websitePage,
|
...websitePage,
|
||||||
@ -273,17 +282,20 @@ const CreateFormPipeline = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (datasourceType === DatasourceType.onlineDrive) {
|
if (datasourceType === DatasourceType.onlineDrive) {
|
||||||
if (datasourceType === DatasourceType.onlineDrive) {
|
const {
|
||||||
selectedFileIds.forEach((id) => {
|
bucket,
|
||||||
const file = onlineDriveFileList.find(file => file.id === id)
|
selectedFileIds,
|
||||||
datasourceInfoList.push({
|
fileList: onlineDriveFileList,
|
||||||
bucket,
|
} = dataSourceStore.getState()
|
||||||
id: file?.id,
|
selectedFileIds.forEach((id) => {
|
||||||
type: file?.type,
|
const file = onlineDriveFileList.find(file => file.id === id)
|
||||||
credential_id: currentCredentialId,
|
datasourceInfoList.push({
|
||||||
})
|
bucket,
|
||||||
|
id: file?.id,
|
||||||
|
type: file?.type,
|
||||||
|
credential_id: currentCredentialId,
|
||||||
})
|
})
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
await runPublishedPipeline({
|
await runPublishedPipeline({
|
||||||
pipeline_id: pipelineId!,
|
pipeline_id: pipelineId!,
|
||||||
@ -299,7 +311,7 @@ const CreateFormPipeline = () => {
|
|||||||
handleNextStep()
|
handleNextStep()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}, [dataSourceStore, datasource, datasourceType, fileList, handleNextStep, onlineDocuments, pipelineId, runPublishedPipeline, selectedFileIds, websitePages])
|
}, [dataSourceStore, datasource, datasourceType, handleNextStep, pipelineId, runPublishedPipeline])
|
||||||
|
|
||||||
const onClickProcess = useCallback(() => {
|
const onClickProcess = useCallback(() => {
|
||||||
isPreview.current = false
|
isPreview.current = false
|
||||||
|
|||||||
@ -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 { PreviewSlice } from '@/app/components/datasets/formatted-text/flavours/preview-slice'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { formatNumber } from '@/utils/format'
|
import { formatNumber } from '@/utils/format'
|
||||||
|
import cn from '@/utils/classnames'
|
||||||
|
|
||||||
enum QAItemType {
|
enum QAItemType {
|
||||||
Question = 'question',
|
Question = 'question',
|
||||||
Answer = 'answer',
|
Answer = 'answer',
|
||||||
}
|
}
|
||||||
|
|
||||||
type QAItemProps = {
|
type QAItemProps = {
|
||||||
type: QAItemType
|
type: QAItemType
|
||||||
text: string
|
text: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const QAItem = (props: QAItemProps) => {
|
const QAItem = (props: QAItemProps) => {
|
||||||
const { type, text } = props
|
const { type, text } = props
|
||||||
return <div className="inline-flex items-start justify-start gap-1 self-stretch">
|
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='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 className='body-md-regular flex-1 text-text-secondary'>{text}</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ChunkType {
|
enum ChunkType {
|
||||||
General = 'genaral',
|
General = 'general',
|
||||||
Paragraph = 'paragraph',
|
Paragraph = 'paragraph',
|
||||||
FullDoc = 'full-doc',
|
FullDoc = 'full-doc',
|
||||||
QA = 'qa',
|
QA = 'qa',
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChunkCardProps = {
|
type ChunkCardProps = {
|
||||||
type: ChunkType
|
type: ChunkType
|
||||||
content: string | string[] | QAChunk
|
content: string | string[] | QAChunk
|
||||||
positionId?: string | number
|
positionId?: string | number
|
||||||
wordCount: number
|
wordCount: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const ChunkCard = (props: ChunkCardProps) => {
|
const ChunkCard = (props: ChunkCardProps) => {
|
||||||
const { type, content, positionId, wordCount } = props
|
const { type, content, positionId, wordCount } = props
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const renderContent = () => {
|
const renderContent = () => {
|
||||||
// ChunkType.Paragraph && ChunkType.FullDoc
|
// ChunkType.Paragraph && ChunkType.FullDoc
|
||||||
if (Array.isArray(content)) {
|
if (Array.isArray(content)) {
|
||||||
return content.map((child, index) => {
|
return content.map((child, index) => {
|
||||||
const indexForLabel = index + 1
|
const indexForLabel = index + 1
|
||||||
return (
|
return (
|
||||||
<PreviewSlice
|
<PreviewSlice
|
||||||
key={child}
|
key={child}
|
||||||
label={`C-${indexForLabel}`}
|
label={`C-${indexForLabel}`}
|
||||||
text={child}
|
text={child}
|
||||||
tooltip={`Child-chunk-${indexForLabel} · ${child.length} Characters`}
|
tooltip={`Child-chunk-${indexForLabel} · ${child.length} Characters`}
|
||||||
labelInnerClassName='text-[10px] font-semibold align-bottom leading-7'
|
labelInnerClassName='text-[10px] font-semibold align-bottom leading-7'
|
||||||
dividerClassName='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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div className="inline-flex flex-col gap-1 self-stretch rounded-lg bg-components-panel-bg px-3 py-2.5">
|
// ChunkType.QA
|
||||||
{type !== ChunkType.FullDoc && <div className="inline-flex items-center justify-start gap-2">
|
if (typeof content === 'object') {
|
||||||
<SegmentIndexTag
|
return <div className='flex flex-col gap-2'>
|
||||||
positionId={positionId}
|
<QAItem type={QAItemType.Question} text={(content as QAChunk).question} />
|
||||||
labelPrefix={type === ChunkType.Paragraph ? 'Parent-Chunk' : 'Chunk'}
|
<QAItem type={QAItemType.Answer} text={(content as QAChunk).answer} />
|
||||||
/>
|
</div>
|
||||||
<Dot />
|
}
|
||||||
<div className='system-xs-medium text-text-tertiary'>{formatNumber(wordCount)} {t('datasetDocuments.segment.characters', { count: wordCount })}</div>
|
|
||||||
</div>}
|
// ChunkType.General
|
||||||
<div className="body-md-regular text-text-secondary">{renderContent()}</div>
|
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>
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ChunkInfo = {
|
export type ChunkInfo = {
|
||||||
general_chunks?: string[]
|
general_chunks?: string[]
|
||||||
parent_child_chunks?: ParentChildChunk[]
|
parent_child_chunks?: ParentChildChunk[]
|
||||||
parent_mode?: string
|
parent_mode?: string
|
||||||
qa_chunks?: QAChunk[]
|
qa_chunks?: QAChunk[]
|
||||||
}
|
}
|
||||||
|
|
||||||
type ParentChildChunk = {
|
type ParentChildChunk = {
|
||||||
child_contents: string[]
|
child_contents: string[]
|
||||||
parent_content: string
|
parent_content: string
|
||||||
parent_mode: string
|
parent_mode: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type QAChunk = {
|
type QAChunk = {
|
||||||
question: string
|
question: string
|
||||||
answer: string
|
answer: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChunkCardListProps = {
|
type ChunkCardListProps = {
|
||||||
chunkInfo: ChunkInfo
|
chunkInfo: ChunkInfo
|
||||||
|
className?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ChunkCardList = (props: ChunkCardListProps) => {
|
export const ChunkCardList = (props: ChunkCardListProps) => {
|
||||||
const { chunkInfo } = props
|
const { chunkInfo, className } = props
|
||||||
|
|
||||||
const chunkType = useMemo(() => {
|
const chunkType = useMemo(() => {
|
||||||
if (chunkInfo?.general_chunks)
|
if (chunkInfo?.general_chunks)
|
||||||
return ChunkType.General
|
return ChunkType.General
|
||||||
|
|
||||||
if (chunkInfo?.parent_child_chunks)
|
if (chunkInfo?.parent_child_chunks)
|
||||||
return chunkInfo.parent_mode as ChunkType
|
return chunkInfo.parent_mode as ChunkType
|
||||||
|
|
||||||
return ChunkType.QA
|
return ChunkType.QA
|
||||||
}, [chunkInfo])
|
}, [chunkInfo])
|
||||||
|
|
||||||
return <div className='flex grow flex-col gap-1'>
|
const chunkList = useMemo(() => {
|
||||||
{(chunkInfo.general_chunks ?? chunkInfo.parent_child_chunks ?? chunkInfo?.qa_chunks ?? []).map((seg: string | ParentChildChunk | QAChunk, index: number) => {
|
if (chunkInfo?.general_chunks)
|
||||||
const isParentChildMode = [ChunkType.Paragraph, ChunkType.FullDoc].includes(chunkType!)
|
return chunkInfo.general_chunks
|
||||||
let wordCount = 0
|
if (chunkInfo?.parent_child_chunks)
|
||||||
if (isParentChildMode)
|
return chunkInfo.parent_child_chunks
|
||||||
wordCount = (seg as ParentChildChunk)?.parent_content?.length
|
return chunkInfo?.qa_chunks ?? []
|
||||||
else if (typeof seg === 'string')
|
}, [chunkInfo])
|
||||||
wordCount = seg.length
|
|
||||||
else
|
|
||||||
wordCount = (seg as QAChunk)?.question?.length + (seg as QAChunk)?.answer?.length
|
|
||||||
|
|
||||||
return <ChunkCard
|
return (
|
||||||
type={chunkType}
|
<div className={cn('flex w-full flex-col gap-y-1', className)}>
|
||||||
content={isParentChildMode ? (seg as ParentChildChunk).child_contents : (seg as string | QAChunk)}
|
{chunkList.map((seg: string | ParentChildChunk | QAChunk, index: number) => {
|
||||||
wordCount={wordCount}
|
const isParentChildMode = [ChunkType.Paragraph, ChunkType.FullDoc].includes(chunkType!)
|
||||||
positionId={index + 1}
|
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>
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
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 Form from './form'
|
||||||
import type { Datasource } from '../../test-run/types'
|
import type { Datasource } from '../../test-run/types'
|
||||||
import { useStore } from '@/app/components/workflow/store'
|
import { useStore } from '@/app/components/workflow/store'
|
||||||
|
|||||||
@ -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)
|
|
||||||
@ -1,24 +1,36 @@
|
|||||||
import React from 'react'
|
import React, { useCallback } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
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 = {
|
const Header = () => {
|
||||||
steps: { label: string; value: string }[]
|
const workflowStore = useWorkflowStore()
|
||||||
currentStep: number
|
|
||||||
}
|
|
||||||
|
|
||||||
const Header = ({
|
|
||||||
steps,
|
|
||||||
currentStep,
|
|
||||||
}: HeaderProps) => {
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const { handleCancelDebugAndPreviewPanel } = useWorkflowInteractions()
|
||||||
|
|
||||||
|
const handleClose = useCallback(() => {
|
||||||
|
const {
|
||||||
|
isPreparingDataSource,
|
||||||
|
setIsPreparingDataSource,
|
||||||
|
} = workflowStore.getState()
|
||||||
|
isPreparingDataSource && setIsPreparingDataSource?.(false)
|
||||||
|
handleCancelDebugAndPreviewPanel()
|
||||||
|
}, [workflowStore])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col gap-y-0.5 px-3 pb-2 pt-3.5'>
|
<div className='flex items-center gap-x-2 pl-4 pr-3 pt-4'>
|
||||||
<div className='system-md-semibold-uppercase flex items-center gap-x-1 pl-1 pr-8 text-text-primary'>
|
<div className='system-xl-semibold grow pl-1 pr-8 text-text-primary'>
|
||||||
{t('datasetPipeline.testRun.title')}
|
{t('datasetPipeline.testRun.title')}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,226 +1,26 @@
|
|||||||
import { useStore as useWorkflowStoreWithSelector } from '@/app/components/workflow/store'
|
import { useStore } 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 DataSourceProvider from '@/app/components/datasets/documents/create-from-pipeline/data-source/store/provider'
|
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 Preparation from './preparation'
|
||||||
import { useShallow } from 'zustand/react/shallow'
|
import Result from './result'
|
||||||
|
import Header from './header'
|
||||||
|
|
||||||
const TestRunPanel = () => {
|
const TestRunPanel = () => {
|
||||||
const setShowDebugAndPreviewPanel = useWorkflowStoreWithSelector(state => state.setShowDebugAndPreviewPanel)
|
const isPreparingDataSource = useStore(state => state.isPreparingDataSource)
|
||||||
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])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<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'
|
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 />
|
||||||
<Header steps={steps} currentStep={currentStep} />
|
{isPreparingDataSource ? (
|
||||||
<div className='flex grow flex-col overflow-y-auto'>
|
<DataSourceProvider>
|
||||||
{
|
<Preparation />
|
||||||
currentStep === 1 && (
|
</DataSourceProvider>
|
||||||
<>
|
) : (
|
||||||
<div className='flex flex-col gap-y-4 px-4 py-2'>
|
<Result />
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const TestRunPanelWrapper = () => {
|
export default TestRunPanel
|
||||||
return (
|
|
||||||
<DataSourceProvider>
|
|
||||||
<TestRunPanel />
|
|
||||||
</DataSourceProvider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default TestRunPanelWrapper
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { useCallback, useEffect } from 'react'
|
import { useCallback, useEffect } from 'react'
|
||||||
import { useDatasourceOptions } from '../hooks'
|
import { useDatasourceOptions } from '../hooks'
|
||||||
import OptionCard from './option-card'
|
import OptionCard from './option-card'
|
||||||
import type { Datasource } from '../types'
|
import type { Datasource } from '../../types'
|
||||||
|
|
||||||
type DataSourceOptionsProps = {
|
type DataSourceOptionsProps = {
|
||||||
dataSourceNodeId: string
|
dataSourceNodeId: string
|
||||||
@ -28,7 +28,6 @@ const DataSourceOptions = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (options.length > 0 && !dataSourceNodeId)
|
if (options.length > 0 && !dataSourceNodeId)
|
||||||
handelSelect(options[0].value)
|
handelSelect(options[0].value)
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import type { DataSourceOption } from './types'
|
import type { DataSourceOption } from '../types'
|
||||||
import { TestRunStep } from './types'
|
import { TestRunStep } from '../types'
|
||||||
import { useNodes } from 'reactflow'
|
import { useNodes } from 'reactflow'
|
||||||
import { BlockEnum } from '@/app/components/workflow/types'
|
import { BlockEnum } from '@/app/components/workflow/types'
|
||||||
import type { DataSourceNodeType } from '@/app/components/workflow/nodes/data-source/types'
|
import type { DataSourceNodeType } from '@/app/components/workflow/nodes/data-source/types'
|
||||||
@ -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)
|
||||||
@ -17,7 +17,7 @@ const StepIndicator = ({
|
|||||||
steps,
|
steps,
|
||||||
}: StepIndicatorProps) => {
|
}: StepIndicatorProps) => {
|
||||||
return (
|
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) => {
|
{steps.map((step, index) => {
|
||||||
const isCurrentStep = index === currentStep - 1
|
const isCurrentStep = index === currentStep - 1
|
||||||
const isLastStep = index === steps.length - 1
|
const isLastStep = index === steps.length - 1
|
||||||
@ -26,7 +26,7 @@ const StepIndicator = ({
|
|||||||
<div
|
<div
|
||||||
className={cn('flex items-center gap-x-1', isCurrentStep ? 'text-state-accent-solid' : 'text-text-tertiary')}
|
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>
|
<span className='system-2xs-semibold-uppercase'>{step.label}</span>
|
||||||
</div>
|
</div>
|
||||||
{!isLastStep && (
|
{!isLastStep && (
|
||||||
@ -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)
|
||||||
@ -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)
|
||||||
@ -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)
|
||||||
@ -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)
|
||||||
@ -1,6 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
memo,
|
memo,
|
||||||
useCallback,
|
|
||||||
useMemo,
|
useMemo,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
@ -9,14 +8,13 @@ import Header from '@/app/components/workflow/header'
|
|||||||
import { fetchWorkflowRunHistory } from '@/service/workflow'
|
import { fetchWorkflowRunHistory } from '@/service/workflow'
|
||||||
import {
|
import {
|
||||||
useStore,
|
useStore,
|
||||||
useWorkflowStore,
|
|
||||||
} from '@/app/components/workflow/store'
|
} from '@/app/components/workflow/store'
|
||||||
import InputFieldButton from './input-field-button'
|
import InputFieldButton from './input-field-button'
|
||||||
import Publisher from './publisher'
|
import Publisher from './publisher'
|
||||||
|
import RunMode from './run-mode'
|
||||||
|
|
||||||
const RagPipelineHeader = () => {
|
const RagPipelineHeader = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const workflowStore = useWorkflowStore()
|
|
||||||
const pipelineId = useStore(s => s.pipelineId)
|
const pipelineId = useStore(s => s.pipelineId)
|
||||||
const showDebugAndPreviewPanel = useStore(s => s.showDebugAndPreviewPanel)
|
const showDebugAndPreviewPanel = useStore(s => s.showDebugAndPreviewPanel)
|
||||||
|
|
||||||
@ -27,11 +25,6 @@ const RagPipelineHeader = () => {
|
|||||||
}
|
}
|
||||||
}, [pipelineId])
|
}, [pipelineId])
|
||||||
|
|
||||||
const handleStopRun = useCallback(() => {
|
|
||||||
const { setShowDebugAndPreviewPanel } = workflowStore.getState()
|
|
||||||
setShowDebugAndPreviewPanel(false)
|
|
||||||
}, [workflowStore])
|
|
||||||
|
|
||||||
const headerProps: HeaderProps = useMemo(() => {
|
const headerProps: HeaderProps = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
normal: {
|
normal: {
|
||||||
@ -41,17 +34,17 @@ const RagPipelineHeader = () => {
|
|||||||
},
|
},
|
||||||
runAndHistoryProps: {
|
runAndHistoryProps: {
|
||||||
showRunButton: true,
|
showRunButton: true,
|
||||||
runButtonText: t('workflow.singleRun.testRun'),
|
|
||||||
viewHistoryProps,
|
viewHistoryProps,
|
||||||
isRunning: showDebugAndPreviewPanel,
|
components: {
|
||||||
onStopRun: handleStopRun,
|
RunMode,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
viewHistory: {
|
viewHistory: {
|
||||||
viewHistoryProps,
|
viewHistoryProps,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}, [viewHistoryProps, showDebugAndPreviewPanel, handleStopRun, t])
|
}, [viewHistoryProps, showDebugAndPreviewPanel, t])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Header {...headerProps} />
|
<Header {...headerProps} />
|
||||||
|
|||||||
@ -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)
|
||||||
@ -287,9 +287,10 @@ export const usePipelineRun = () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const handleStopRun = useCallback((taskId: string) => {
|
const handleStopRun = useCallback((taskId: string) => {
|
||||||
const { pipelineId } = workflowStore.getState()
|
const { pipelineId, setShowDebugAndPreviewPanel } = workflowStore.getState()
|
||||||
|
|
||||||
stopWorkflowRun(`/rag/pipeline/${pipelineId}/workflow-runs/tasks/${taskId}/stop`)
|
stopWorkflowRun(`/rag/pipeline/${pipelineId}/workflow-runs/tasks/${taskId}/stop`)
|
||||||
|
setShowDebugAndPreviewPanel(false)
|
||||||
}, [workflowStore])
|
}, [workflowStore])
|
||||||
|
|
||||||
const handleRestoreFromPublishedWorkflow = useCallback((publishedWorkflow: VersionHistory) => {
|
const handleRestoreFromPublishedWorkflow = useCallback((publishedWorkflow: VersionHistory) => {
|
||||||
|
|||||||
@ -24,20 +24,32 @@ export const usePipelineStartRun = () => {
|
|||||||
return
|
return
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
isPreparingDataSource,
|
||||||
|
setIsPreparingDataSource,
|
||||||
showDebugAndPreviewPanel,
|
showDebugAndPreviewPanel,
|
||||||
setShowEnvPanel,
|
setShowEnvPanel,
|
||||||
setShowDebugAndPreviewPanel,
|
setShowDebugAndPreviewPanel,
|
||||||
} = workflowStore.getState()
|
} = workflowStore.getState()
|
||||||
|
|
||||||
|
if (!isPreparingDataSource && workflowRunningData) {
|
||||||
|
workflowStore.setState({
|
||||||
|
isPreparingDataSource: true,
|
||||||
|
workflowRunningData: undefined,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
setShowEnvPanel(false)
|
setShowEnvPanel(false)
|
||||||
closeAllInputFieldPanels()
|
closeAllInputFieldPanels()
|
||||||
|
|
||||||
if (showDebugAndPreviewPanel) {
|
if (showDebugAndPreviewPanel) {
|
||||||
|
setIsPreparingDataSource?.(false)
|
||||||
handleCancelDebugAndPreviewPanel()
|
handleCancelDebugAndPreviewPanel()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await doSyncWorkflowDraft()
|
await doSyncWorkflowDraft()
|
||||||
|
setIsPreparingDataSource?.(true)
|
||||||
setShowDebugAndPreviewPanel(true)
|
setShowDebugAndPreviewPanel(true)
|
||||||
}, [workflowStore, handleCancelDebugAndPreviewPanel, doSyncWorkflowDraft])
|
}, [workflowStore, handleCancelDebugAndPreviewPanel, doSyncWorkflowDraft])
|
||||||
|
|
||||||
|
|||||||
@ -24,6 +24,8 @@ export type RagPipelineSliceShape = {
|
|||||||
setRagPipelineVariables: (ragPipelineVariables: RAGPipelineVariables) => void
|
setRagPipelineVariables: (ragPipelineVariables: RAGPipelineVariables) => void
|
||||||
dataSourceList: ToolWithProvider[]
|
dataSourceList: ToolWithProvider[]
|
||||||
setDataSourceList: (dataSourceList: DataSourceItem[]) => void
|
setDataSourceList: (dataSourceList: DataSourceItem[]) => void
|
||||||
|
isPreparingDataSource: boolean
|
||||||
|
setIsPreparingDataSource: (isPreparingDataSource: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CreateRagPipelineSliceSlice = StateCreator<RagPipelineSliceShape>
|
export type CreateRagPipelineSliceSlice = StateCreator<RagPipelineSliceShape>
|
||||||
@ -45,4 +47,6 @@ export const createRagPipelineSliceSlice: StateCreator<RagPipelineSliceShape> =
|
|||||||
const formattedDataSourceList = dataSourceList.map(item => transformDataSourceToTool(item))
|
const formattedDataSourceList = dataSourceList.map(item => transformDataSourceToTool(item))
|
||||||
set(() => ({ dataSourceList: formattedDataSourceList }))
|
set(() => ({ dataSourceList: formattedDataSourceList }))
|
||||||
},
|
},
|
||||||
|
isPreparingDataSource: false,
|
||||||
|
setIsPreparingDataSource: isPreparingDataSource => set(() => ({ isPreparingDataSource })),
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,97 +1,17 @@
|
|||||||
import { memo } from 'react'
|
import { memo } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import {
|
import {
|
||||||
RiLoader2Line,
|
|
||||||
RiPlayLargeLine,
|
RiPlayLargeLine,
|
||||||
} from '@remixicon/react'
|
} from '@remixicon/react'
|
||||||
import { useStore } from '../store'
|
|
||||||
import {
|
import {
|
||||||
useNodesReadOnly,
|
useNodesReadOnly,
|
||||||
useWorkflowRun,
|
|
||||||
useWorkflowStartRun,
|
useWorkflowStartRun,
|
||||||
} from '../hooks'
|
} from '../hooks'
|
||||||
import { WorkflowRunningStatus } from '../types'
|
|
||||||
import type { ViewHistoryProps } from './view-history'
|
import type { ViewHistoryProps } from './view-history'
|
||||||
import ViewHistory from './view-history'
|
import ViewHistory from './view-history'
|
||||||
import Checklist from './checklist'
|
import Checklist from './checklist'
|
||||||
import cn from '@/utils/classnames'
|
import cn from '@/utils/classnames'
|
||||||
import {
|
import RunMode from './run-mode'
|
||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const PreviewMode = memo(() => {
|
const PreviewMode = memo(() => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@ -115,24 +35,32 @@ export type RunAndHistoryProps = {
|
|||||||
showRunButton?: boolean
|
showRunButton?: boolean
|
||||||
runButtonText?: string
|
runButtonText?: string
|
||||||
isRunning?: boolean
|
isRunning?: boolean
|
||||||
onStopRun?: () => void
|
|
||||||
showPreviewButton?: boolean
|
showPreviewButton?: boolean
|
||||||
viewHistoryProps?: ViewHistoryProps
|
viewHistoryProps?: ViewHistoryProps
|
||||||
|
components?: {
|
||||||
|
RunMode?: React.ComponentType<
|
||||||
|
{
|
||||||
|
text?: string
|
||||||
|
}
|
||||||
|
>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const RunAndHistory = ({
|
const RunAndHistory = ({
|
||||||
showRunButton,
|
showRunButton,
|
||||||
runButtonText,
|
runButtonText,
|
||||||
isRunning,
|
|
||||||
onStopRun,
|
|
||||||
showPreviewButton,
|
showPreviewButton,
|
||||||
viewHistoryProps,
|
viewHistoryProps,
|
||||||
|
components,
|
||||||
}: RunAndHistoryProps) => {
|
}: RunAndHistoryProps) => {
|
||||||
const { nodesReadOnly } = useNodesReadOnly()
|
const { nodesReadOnly } = useNodesReadOnly()
|
||||||
|
const { RunMode: CustomRunMode } = components || {}
|
||||||
|
|
||||||
return (
|
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'>
|
<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 />
|
showPreviewButton && <PreviewMode />
|
||||||
|
|||||||
96
web/app/components/workflow/header/run-mode.tsx
Normal file
96
web/app/components/workflow/header/run-mode.tsx
Normal 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)
|
||||||
@ -72,7 +72,7 @@ const DisplayContent = (props: DisplayContentProps) => {
|
|||||||
btnClassName='!pl-1.5 !pr-0.5 gap-[3px]'
|
btnClassName='!pl-1.5 !pr-0.5 gap-[3px]'
|
||||||
/>
|
/>
|
||||||
</div>
|
</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 && (
|
{viewMode === ViewMode.Code && (
|
||||||
type === ContentType.Markdown
|
type === ContentType.Markdown
|
||||||
? <Textarea
|
? <Textarea
|
||||||
|
|||||||
@ -10,6 +10,10 @@ const translation = {
|
|||||||
description: 'Knowledge description',
|
description: 'Knowledge description',
|
||||||
descriptionPlaceholder: 'Please enter the description of this Knowledge Pipeline. (Optional) ',
|
descriptionPlaceholder: 'Please enter the description of this Knowledge Pipeline. (Optional) ',
|
||||||
},
|
},
|
||||||
|
testRun: 'Test Run',
|
||||||
|
preparingDataSource: 'Preparing Data Source',
|
||||||
|
reRun: 'Re-run',
|
||||||
|
processing: 'Processing',
|
||||||
},
|
},
|
||||||
inputField: {
|
inputField: {
|
||||||
create: 'Create user input field',
|
create: 'Create user input field',
|
||||||
@ -19,6 +23,14 @@ const translation = {
|
|||||||
title: 'This pipeline has not yet been published',
|
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.',
|
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
|
export default translation
|
||||||
|
|||||||
@ -212,8 +212,10 @@ const translation = {
|
|||||||
toolParameterRequired: '{{field}}: parameter [{{param}}] is required',
|
toolParameterRequired: '{{field}}: parameter [{{param}}] is required',
|
||||||
},
|
},
|
||||||
singleRun: {
|
singleRun: {
|
||||||
testRun: 'Test Run ',
|
testRun: 'Test Run',
|
||||||
startRun: 'Start Run',
|
startRun: 'Start Run',
|
||||||
|
preparingDataSource: 'Preparing Data Source',
|
||||||
|
reRun: 'Re-run',
|
||||||
running: 'Running',
|
running: 'Running',
|
||||||
testRunIteration: 'Test Run Iteration',
|
testRunIteration: 'Test Run Iteration',
|
||||||
back: 'Back',
|
back: 'Back',
|
||||||
@ -974,7 +976,7 @@ const translation = {
|
|||||||
updateSuccess: 'Version updated',
|
updateSuccess: 'Version updated',
|
||||||
updateFailure: 'Failed to update version',
|
updateFailure: 'Failed to update version',
|
||||||
copyIdSuccess: 'ID copied to clipboard',
|
copyIdSuccess: 'ID copied to clipboard',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
debug: {
|
debug: {
|
||||||
settingsTab: 'Settings',
|
settingsTab: 'Settings',
|
||||||
|
|||||||
@ -10,6 +10,10 @@ const translation = {
|
|||||||
description: '知识流水线描述',
|
description: '知识流水线描述',
|
||||||
descriptionPlaceholder: '请输入此知识流水线的描述。 (可选)',
|
descriptionPlaceholder: '请输入此知识流水线的描述。 (可选)',
|
||||||
},
|
},
|
||||||
|
testRun: '测试运行',
|
||||||
|
preparingDataSource: '准备数据源',
|
||||||
|
reRun: '重新运行',
|
||||||
|
processing: '处理中',
|
||||||
},
|
},
|
||||||
inputField: {
|
inputField: {
|
||||||
create: '创建用户输入字段',
|
create: '创建用户输入字段',
|
||||||
@ -19,6 +23,14 @@ const translation = {
|
|||||||
title: '此知识流水线尚未发布',
|
title: '此知识流水线尚未发布',
|
||||||
desc: '当知识流水线未发布时,您可以修改知识库节点中的分块结构,知识流水线编排和更改将自动保存为草稿。',
|
desc: '当知识流水线未发布时,您可以修改知识库节点中的分块结构,知识流水线编排和更改将自动保存为草稿。',
|
||||||
},
|
},
|
||||||
|
result: {
|
||||||
|
resultPreview: {
|
||||||
|
loading: '处理中...请稍后',
|
||||||
|
error: '执行过程中出现错误',
|
||||||
|
viewDetails: '查看详情',
|
||||||
|
footerTip: '在测试运行模式下,最多预览 {{count}} 个分段',
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export default translation
|
export default translation
|
||||||
|
|||||||
@ -212,8 +212,10 @@ const translation = {
|
|||||||
toolParameterRequired: '{{field}}: 参数 [{{param}}] 不能为空',
|
toolParameterRequired: '{{field}}: 参数 [{{param}}] 不能为空',
|
||||||
},
|
},
|
||||||
singleRun: {
|
singleRun: {
|
||||||
testRun: '测试运行 ',
|
testRun: '测试运行',
|
||||||
startRun: '开始运行',
|
startRun: '开始运行',
|
||||||
|
preparingDataSource: '准备数据源',
|
||||||
|
reRun: '重新运行',
|
||||||
running: '运行中',
|
running: '运行中',
|
||||||
testRunIteration: '测试运行迭代',
|
testRunIteration: '测试运行迭代',
|
||||||
back: '返回',
|
back: '返回',
|
||||||
@ -937,20 +939,6 @@ const translation = {
|
|||||||
indexMethodIsRequired: '索引方法是必需的',
|
indexMethodIsRequired: '索引方法是必需的',
|
||||||
retrievalSettingIsRequired: '检索设置是必需的',
|
retrievalSettingIsRequired: '检索设置是必需的',
|
||||||
},
|
},
|
||||||
dataSource: {
|
|
||||||
supportedFileFormats: '支持的文件格式',
|
|
||||||
supportedFileFormatsPlaceholder: '文件格式,例如:doc',
|
|
||||||
add: '添加数据源',
|
|
||||||
},
|
|
||||||
knowledgeBase: {
|
|
||||||
chunkStructure: '分段结构',
|
|
||||||
chooseChunkStructure: '选择分段结构',
|
|
||||||
changeChunkStructure: '更改分段结构',
|
|
||||||
aboutRetrieval: '关于知识检索。',
|
|
||||||
chunkIsRequired: '分段结构是必需的',
|
|
||||||
indexMethodIsRequired: '索引方法是必需的',
|
|
||||||
retrievalSettingIsRequired: '检索设置是必需的',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
tracing: {
|
tracing: {
|
||||||
stopBy: '由{{user}}终止',
|
stopBy: '由{{user}}终止',
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user