dify
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { QAChunk } from './types'
|
||||
import { QAItemType } from './types'
|
||||
import { PreviewSlice } from '@/app/components/datasets/formatted-text/flavours/preview-slice'
|
||||
import SegmentIndexTag from '@/app/components/datasets/documents/detail/completed/common/segment-index-tag'
|
||||
import Dot from '@/app/components/datasets/documents/detail/completed/common/dot'
|
||||
import { formatNumber } from '@/utils/format'
|
||||
import QAItem from './q-a-item'
|
||||
import { ChunkingMode, type ParentMode } from '@/models/datasets'
|
||||
|
||||
type ChunkCardProps = {
|
||||
chunkType: ChunkingMode
|
||||
parentMode?: ParentMode
|
||||
content: string | string[] | QAChunk
|
||||
positionId?: string | number
|
||||
wordCount: number
|
||||
}
|
||||
|
||||
const ChunkCard = (props: ChunkCardProps) => {
|
||||
const { chunkType, parentMode, content, positionId, wordCount } = props
|
||||
const { t } = useTranslation()
|
||||
|
||||
const isFullDoc = useMemo(() => {
|
||||
return chunkType === ChunkingMode.parentChild && parentMode === 'full-doc'
|
||||
}, [chunkType, parentMode])
|
||||
|
||||
const isParagraph = useMemo(() => {
|
||||
return chunkType === ChunkingMode.parentChild && parentMode === 'paragraph'
|
||||
}, [chunkType, parentMode])
|
||||
|
||||
const contentElement = useMemo(() => {
|
||||
if (chunkType === ChunkingMode.parentChild) {
|
||||
return (content as string[]).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'
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
if (chunkType === ChunkingMode.qa) {
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
return content as string
|
||||
}, [content, chunkType])
|
||||
|
||||
return (
|
||||
<div className='flex flex-col gap-1 rounded-lg bg-components-panel-bg px-3 py-2.5'>
|
||||
{!isFullDoc && (
|
||||
<div className='inline-flex items-center justify-start gap-2'>
|
||||
<SegmentIndexTag
|
||||
positionId={positionId}
|
||||
labelPrefix={isParagraph ? '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'>{contentElement}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(ChunkCard)
|
||||
@@ -0,0 +1,51 @@
|
||||
import { useMemo } from 'react'
|
||||
import cn from '@/utils/classnames'
|
||||
import type { ChunkInfo, GeneralChunks, ParentChildChunk, ParentChildChunks, QAChunk, QAChunks } from './types'
|
||||
import { ChunkingMode, type ParentMode } from '@/models/datasets'
|
||||
import ChunkCard from './chunk-card'
|
||||
|
||||
type ChunkCardListProps = {
|
||||
chunkType: ChunkingMode
|
||||
parentMode?: ParentMode
|
||||
chunkInfo: ChunkInfo
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const ChunkCardList = (props: ChunkCardListProps) => {
|
||||
const { chunkType, parentMode, chunkInfo, className } = props
|
||||
|
||||
const chunkList = useMemo(() => {
|
||||
if (chunkType === ChunkingMode.text)
|
||||
return chunkInfo as GeneralChunks
|
||||
if (chunkType === ChunkingMode.parentChild)
|
||||
return (chunkInfo as ParentChildChunks).parent_child_chunks
|
||||
return (chunkInfo as QAChunks).qa_chunks
|
||||
}, [chunkInfo])
|
||||
|
||||
const getWordCount = (seg: string | ParentChildChunk | QAChunk) => {
|
||||
if (chunkType === ChunkingMode.parentChild)
|
||||
return (seg as ParentChildChunk).parent_content.length
|
||||
if (chunkType === ChunkingMode.text)
|
||||
return (seg as string).length
|
||||
return (seg as QAChunk).question.length + (seg as QAChunk).answer.length
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('flex w-full flex-col gap-y-1', className)}>
|
||||
{chunkList.map((seg, index: number) => {
|
||||
const wordCount = getWordCount(seg)
|
||||
|
||||
return (
|
||||
<ChunkCard
|
||||
key={`${chunkType}-${index}`}
|
||||
chunkType={chunkType}
|
||||
parentMode={parentMode}
|
||||
content={chunkType === ChunkingMode.parentChild ? (seg as ParentChildChunk).child_contents : (seg as string | QAChunk)}
|
||||
wordCount={wordCount}
|
||||
positionId={index + 1}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import React from 'react'
|
||||
import { QAItemType } from './types'
|
||||
|
||||
type QAItemProps = {
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(QAItem)
|
||||
@@ -0,0 +1,28 @@
|
||||
export type GeneralChunks = string[]
|
||||
|
||||
export type ParentChildChunk = {
|
||||
child_contents: string[]
|
||||
parent_content: string
|
||||
parent_mode: string
|
||||
}
|
||||
|
||||
export type ParentChildChunks = {
|
||||
parent_child_chunks: ParentChildChunk[]
|
||||
parent_mode: string
|
||||
}
|
||||
|
||||
export type QAChunk = {
|
||||
question: string
|
||||
answer: string
|
||||
}
|
||||
|
||||
export type QAChunks = {
|
||||
qa_chunks: QAChunk[]
|
||||
}
|
||||
|
||||
export type ChunkInfo = GeneralChunks | ParentChildChunks | QAChunks
|
||||
|
||||
export enum QAItemType {
|
||||
Question = 'question',
|
||||
Answer = 'answer',
|
||||
}
|
||||
103
dify/web/app/components/rag-pipeline/components/conversion.tsx
Normal file
103
dify/web/app/components/rag-pipeline/components/conversion.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import PipelineScreenShot from './screenshot'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
import { useConvertDatasetToPipeline } from '@/service/use-pipeline'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { useInvalid } from '@/service/use-base'
|
||||
import { datasetDetailQueryKeyPrefix } from '@/service/knowledge/use-dataset'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
|
||||
const Conversion = () => {
|
||||
const { t } = useTranslation()
|
||||
const { datasetId } = useParams()
|
||||
const [showConfirmModal, setShowConfirmModal] = useState(false)
|
||||
|
||||
const { mutateAsync: convert, isPending } = useConvertDatasetToPipeline()
|
||||
const invalidDatasetDetail = useInvalid([...datasetDetailQueryKeyPrefix, datasetId])
|
||||
const handleConvert = useCallback(() => {
|
||||
convert(datasetId as string, {
|
||||
onSuccess: (res) => {
|
||||
if (res.status === 'success') {
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('datasetPipeline.conversion.successMessage'),
|
||||
})
|
||||
setShowConfirmModal(false)
|
||||
invalidDatasetDetail()
|
||||
}
|
||||
else if (res.status === 'failed') {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('datasetPipeline.conversion.errorMessage'),
|
||||
})
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('datasetPipeline.conversion.errorMessage'),
|
||||
})
|
||||
},
|
||||
})
|
||||
}, [convert, datasetId, invalidDatasetDetail, t])
|
||||
|
||||
const handleShowConfirmModal = useCallback(() => {
|
||||
setShowConfirmModal(true)
|
||||
}, [])
|
||||
|
||||
const handleCancelConversion = useCallback(() => {
|
||||
setShowConfirmModal(false)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className='flex h-full w-full items-center justify-center bg-background-body p-6 pb-16'>
|
||||
<div className='flex rounded-2xl border-[0.5px] border-components-card-border bg-components-card-bg shadow-sm shadow-shadow-shadow-4'>
|
||||
<div className='flex max-w-[480px] flex-col justify-between p-10'>
|
||||
<div className='flex flex-col gap-y-2.5'>
|
||||
<div className='title-4xl-semi-bold text-text-primary'>
|
||||
{t('datasetPipeline.conversion.title')}
|
||||
</div>
|
||||
<div className='body-md-medium'>
|
||||
<span className='text-text-secondary'>{t('datasetPipeline.conversion.descriptionChunk1')}</span>
|
||||
<span className='text-text-tertiary'>{t('datasetPipeline.conversion.descriptionChunk2')}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-center gap-x-4'>
|
||||
<Button
|
||||
variant='primary'
|
||||
className='w-32'
|
||||
onClick={handleShowConfirmModal}
|
||||
>
|
||||
{t('datasetPipeline.operations.convert')}
|
||||
</Button>
|
||||
<span className='system-xs-regular text-text-warning'>
|
||||
{t('datasetPipeline.conversion.warning')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className='pb-8 pl-[25px] pr-0 pt-6'>
|
||||
<div className='rounded-l-xl border border-effects-highlight bg-background-default p-1 shadow-md shadow-shadow-shadow-5 backdrop-blur-[5px]'>
|
||||
<div className='overflow-hidden rounded-l-lg'>
|
||||
<PipelineScreenShot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{showConfirmModal && (
|
||||
<Confirm
|
||||
title={t('datasetPipeline.conversion.confirm.title')}
|
||||
content={t('datasetPipeline.conversion.confirm.content')}
|
||||
isShow={showConfirmModal}
|
||||
onConfirm={handleConvert}
|
||||
onCancel={handleCancelConversion}
|
||||
isLoading={isPending}
|
||||
isDisabled={isPending}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Conversion)
|
||||
@@ -0,0 +1,85 @@
|
||||
import {
|
||||
memo,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import type { PanelProps } from '@/app/components/workflow/panel'
|
||||
import Panel from '@/app/components/workflow/panel'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import dynamic from 'next/dynamic'
|
||||
|
||||
const Record = dynamic(() => import('@/app/components/workflow/panel/record'), {
|
||||
ssr: false,
|
||||
})
|
||||
const TestRunPanel = dynamic(() => import('@/app/components/rag-pipeline/components/panel/test-run'), {
|
||||
ssr: false,
|
||||
})
|
||||
const InputFieldPanel = dynamic(() => import('./input-field'), {
|
||||
ssr: false,
|
||||
})
|
||||
const InputFieldEditorPanel = dynamic(() => import('./input-field/editor'), {
|
||||
ssr: false,
|
||||
})
|
||||
const PreviewPanel = dynamic(() => import('./input-field/preview'), {
|
||||
ssr: false,
|
||||
})
|
||||
const GlobalVariablePanel = dynamic(() => import('@/app/components/workflow/panel/global-variable-panel'), {
|
||||
ssr: false,
|
||||
})
|
||||
const RagPipelinePanelOnRight = () => {
|
||||
const historyWorkflowData = useStore(s => s.historyWorkflowData)
|
||||
const showDebugAndPreviewPanel = useStore(s => s.showDebugAndPreviewPanel)
|
||||
const showGlobalVariablePanel = useStore(s => s.showGlobalVariablePanel)
|
||||
|
||||
return (
|
||||
<>
|
||||
{historyWorkflowData && <Record />}
|
||||
{showDebugAndPreviewPanel && <TestRunPanel />}
|
||||
{showGlobalVariablePanel && <GlobalVariablePanel />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const RagPipelinePanelOnLeft = () => {
|
||||
const showInputFieldPanel = useStore(s => s.showInputFieldPanel)
|
||||
const showInputFieldPreviewPanel = useStore(s => s.showInputFieldPreviewPanel)
|
||||
const inputFieldEditPanelProps = useStore(s => s.inputFieldEditPanelProps)
|
||||
return (
|
||||
<>
|
||||
{showInputFieldPreviewPanel && <PreviewPanel />}
|
||||
{inputFieldEditPanelProps && (
|
||||
<InputFieldEditorPanel
|
||||
{...inputFieldEditPanelProps}
|
||||
/>
|
||||
)}
|
||||
{showInputFieldPanel && <InputFieldPanel />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const RagPipelinePanel = () => {
|
||||
const pipelineId = useStore(s => s.pipelineId)
|
||||
const versionHistoryPanelProps = useMemo(() => {
|
||||
return {
|
||||
getVersionListUrl: `/rag/pipelines/${pipelineId}/workflows`,
|
||||
deleteVersionUrl: (versionId: string) => `/rag/pipelines/${pipelineId}/workflows/${versionId}`,
|
||||
updateVersionUrl: (versionId: string) => `/rag/pipelines/${pipelineId}/workflows/${versionId}`,
|
||||
latestVersionId: '',
|
||||
}
|
||||
}, [pipelineId])
|
||||
|
||||
const panelProps: PanelProps = useMemo(() => {
|
||||
return {
|
||||
components: {
|
||||
left: <RagPipelinePanelOnLeft />,
|
||||
right: <RagPipelinePanelOnRight />,
|
||||
},
|
||||
versionHistoryPanelProps,
|
||||
}
|
||||
}, [versionHistoryPanelProps])
|
||||
|
||||
return (
|
||||
<Panel {...panelProps} />
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(RagPipelinePanel)
|
||||
@@ -0,0 +1,38 @@
|
||||
import React from 'react'
|
||||
import { withForm } from '@/app/components/base/form'
|
||||
import InputField from '@/app/components/base/form/form-scenarios/input-field/field'
|
||||
import { useStore } from '@tanstack/react-form'
|
||||
import { useHiddenConfigurations } from './hooks'
|
||||
|
||||
type HiddenFieldsProps = {
|
||||
initialData?: Record<string, any>
|
||||
}
|
||||
|
||||
const HiddenFields = ({
|
||||
initialData,
|
||||
}: HiddenFieldsProps) => withForm({
|
||||
defaultValues: initialData,
|
||||
render: function Render({
|
||||
form,
|
||||
}) {
|
||||
const options = useStore(form.store, state => state.values.options)
|
||||
|
||||
const hiddenConfigurations = useHiddenConfigurations({
|
||||
options,
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
{hiddenConfigurations.map((config, index) => {
|
||||
const FieldComponent = InputField({
|
||||
initialData,
|
||||
config,
|
||||
})
|
||||
return <FieldComponent key={index} form={form} />
|
||||
})}
|
||||
</>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export default HiddenFields
|
||||
@@ -0,0 +1,365 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import type { InputFieldConfiguration } from '@/app/components/base/form/form-scenarios/input-field/types'
|
||||
import { InputFieldType } from '@/app/components/base/form/form-scenarios/input-field/types'
|
||||
import type { DeepKeys } from '@tanstack/react-form'
|
||||
import { useFileUploadConfig } from '@/service/use-common'
|
||||
import { useFileSizeLimit } from '@/app/components/base/file-uploader/hooks'
|
||||
import { formatFileSize } from '@/utils/format'
|
||||
import { DEFAULT_FILE_UPLOAD_SETTING } from '@/app/components/workflow/constants'
|
||||
import { DEFAULT_VALUE_MAX_LEN } from '@/config'
|
||||
import type { FormData } from './types'
|
||||
import { TEXT_MAX_LENGTH } from './schema'
|
||||
import { PipelineInputVarType } from '@/models/pipeline'
|
||||
|
||||
export const useHiddenFieldNames = (type: PipelineInputVarType) => {
|
||||
const { t } = useTranslation()
|
||||
const hiddenFieldNames = useMemo(() => {
|
||||
let fieldNames = []
|
||||
switch (type) {
|
||||
case PipelineInputVarType.textInput:
|
||||
case PipelineInputVarType.paragraph:
|
||||
fieldNames = [
|
||||
t('appDebug.variableConfig.defaultValue'),
|
||||
t('appDebug.variableConfig.placeholder'),
|
||||
t('appDebug.variableConfig.tooltips'),
|
||||
]
|
||||
break
|
||||
case PipelineInputVarType.number:
|
||||
fieldNames = [
|
||||
t('appDebug.variableConfig.defaultValue'),
|
||||
t('appDebug.variableConfig.unit'),
|
||||
t('appDebug.variableConfig.placeholder'),
|
||||
t('appDebug.variableConfig.tooltips'),
|
||||
]
|
||||
break
|
||||
case PipelineInputVarType.select:
|
||||
fieldNames = [
|
||||
t('appDebug.variableConfig.defaultValue'),
|
||||
t('appDebug.variableConfig.tooltips'),
|
||||
]
|
||||
break
|
||||
case PipelineInputVarType.singleFile:
|
||||
fieldNames = [
|
||||
t('appDebug.variableConfig.uploadMethod'),
|
||||
t('appDebug.variableConfig.tooltips'),
|
||||
]
|
||||
break
|
||||
case PipelineInputVarType.multiFiles:
|
||||
fieldNames = [
|
||||
t('appDebug.variableConfig.uploadMethod'),
|
||||
t('appDebug.variableConfig.maxNumberOfUploads'),
|
||||
t('appDebug.variableConfig.tooltips'),
|
||||
]
|
||||
break
|
||||
case PipelineInputVarType.checkbox:
|
||||
fieldNames = [
|
||||
t('appDebug.variableConfig.startChecked'),
|
||||
t('appDebug.variableConfig.tooltips'),
|
||||
]
|
||||
break
|
||||
default:
|
||||
fieldNames = [
|
||||
t('appDebug.variableConfig.tooltips'),
|
||||
]
|
||||
}
|
||||
return fieldNames.map(name => name.toLowerCase()).join(', ')
|
||||
}, [type, t])
|
||||
|
||||
return hiddenFieldNames
|
||||
}
|
||||
|
||||
export const useConfigurations = (props: {
|
||||
getFieldValue: (fieldName: DeepKeys<FormData>) => any,
|
||||
setFieldValue: (fieldName: DeepKeys<FormData>, value: any) => void,
|
||||
supportFile: boolean
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { getFieldValue, setFieldValue, supportFile } = props
|
||||
|
||||
const handleTypeChange = useCallback((type: PipelineInputVarType) => {
|
||||
if ([PipelineInputVarType.singleFile, PipelineInputVarType.multiFiles].includes(type)) {
|
||||
setFieldValue('allowedFileUploadMethods', DEFAULT_FILE_UPLOAD_SETTING.allowed_file_upload_methods)
|
||||
setFieldValue('allowedTypesAndExtensions', {
|
||||
allowedFileTypes: DEFAULT_FILE_UPLOAD_SETTING.allowed_file_types,
|
||||
allowedFileExtensions: DEFAULT_FILE_UPLOAD_SETTING.allowed_file_extensions,
|
||||
})
|
||||
if (type === PipelineInputVarType.multiFiles)
|
||||
setFieldValue('maxLength', DEFAULT_FILE_UPLOAD_SETTING.max_length)
|
||||
}
|
||||
if (type === PipelineInputVarType.paragraph)
|
||||
setFieldValue('maxLength', DEFAULT_VALUE_MAX_LEN)
|
||||
}, [setFieldValue])
|
||||
|
||||
const handleVariableNameBlur = useCallback((value: string) => {
|
||||
const label = getFieldValue('label')
|
||||
if (!value || label)
|
||||
return
|
||||
setFieldValue('label', value)
|
||||
}, [getFieldValue, setFieldValue])
|
||||
|
||||
const handleDisplayNameBlur = useCallback((value: string) => {
|
||||
if (!value)
|
||||
setFieldValue('label', getFieldValue('variable'))
|
||||
}, [getFieldValue, setFieldValue])
|
||||
|
||||
const initialConfigurations = useMemo((): InputFieldConfiguration[] => {
|
||||
return [{
|
||||
type: InputFieldType.inputTypeSelect,
|
||||
label: t('appDebug.variableConfig.fieldType'),
|
||||
variable: 'type',
|
||||
required: true,
|
||||
showConditions: [],
|
||||
listeners: {
|
||||
onChange: ({ value }) => handleTypeChange(value),
|
||||
},
|
||||
supportFile,
|
||||
}, {
|
||||
type: InputFieldType.textInput,
|
||||
label: t('appDebug.variableConfig.varName'),
|
||||
variable: 'variable',
|
||||
placeholder: t('appDebug.variableConfig.inputPlaceholder'),
|
||||
required: true,
|
||||
listeners: {
|
||||
onBlur: ({ value }) => handleVariableNameBlur(value),
|
||||
},
|
||||
showConditions: [],
|
||||
}, {
|
||||
type: InputFieldType.textInput,
|
||||
label: t('appDebug.variableConfig.displayName'),
|
||||
variable: 'label',
|
||||
placeholder: t('appDebug.variableConfig.inputPlaceholder'),
|
||||
required: false,
|
||||
listeners: {
|
||||
onBlur: ({ value }) => handleDisplayNameBlur(value),
|
||||
},
|
||||
showConditions: [],
|
||||
}, {
|
||||
type: InputFieldType.numberInput,
|
||||
label: t('appDebug.variableConfig.maxLength'),
|
||||
variable: 'maxLength',
|
||||
placeholder: t('appDebug.variableConfig.inputPlaceholder'),
|
||||
required: true,
|
||||
showConditions: [{
|
||||
variable: 'type',
|
||||
value: PipelineInputVarType.textInput,
|
||||
}],
|
||||
min: 1,
|
||||
max: TEXT_MAX_LENGTH,
|
||||
}, {
|
||||
type: InputFieldType.options,
|
||||
label: t('appDebug.variableConfig.options'),
|
||||
variable: 'options',
|
||||
required: true,
|
||||
showConditions: [{
|
||||
variable: 'type',
|
||||
value: PipelineInputVarType.select,
|
||||
}],
|
||||
}, {
|
||||
type: InputFieldType.fileTypes,
|
||||
label: t('appDebug.variableConfig.file.supportFileTypes'),
|
||||
variable: 'allowedTypesAndExtensions',
|
||||
required: true,
|
||||
showConditions: [{
|
||||
variable: 'type',
|
||||
value: PipelineInputVarType.singleFile,
|
||||
}],
|
||||
}, {
|
||||
type: InputFieldType.fileTypes,
|
||||
label: t('appDebug.variableConfig.file.supportFileTypes'),
|
||||
variable: 'allowedTypesAndExtensions',
|
||||
required: true,
|
||||
showConditions: [{
|
||||
variable: 'type',
|
||||
value: PipelineInputVarType.multiFiles,
|
||||
}],
|
||||
}, {
|
||||
type: InputFieldType.checkbox,
|
||||
label: t('appDebug.variableConfig.required'),
|
||||
variable: 'required',
|
||||
required: true,
|
||||
showConditions: [],
|
||||
}]
|
||||
}, [t, supportFile, handleTypeChange, handleVariableNameBlur, handleDisplayNameBlur])
|
||||
|
||||
return initialConfigurations
|
||||
}
|
||||
|
||||
export const useHiddenConfigurations = (props: {
|
||||
options: string[] | undefined,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { options } = props
|
||||
|
||||
const { data: fileUploadConfigResponse } = useFileUploadConfig()
|
||||
const {
|
||||
imgSizeLimit,
|
||||
docSizeLimit,
|
||||
audioSizeLimit,
|
||||
videoSizeLimit,
|
||||
} = useFileSizeLimit(fileUploadConfigResponse)
|
||||
|
||||
const defaultSelectOptions = useMemo(() => {
|
||||
if (options) {
|
||||
const defaultOptions = [
|
||||
{
|
||||
value: '',
|
||||
label: t('appDebug.variableConfig.noDefaultSelected'),
|
||||
},
|
||||
]
|
||||
const otherOptions = options.map((option: string) => ({
|
||||
value: option,
|
||||
label: option,
|
||||
}))
|
||||
return [...defaultOptions, ...otherOptions]
|
||||
}
|
||||
return []
|
||||
}, [options, t])
|
||||
|
||||
const hiddenConfigurations = useMemo((): InputFieldConfiguration[] => {
|
||||
return [{
|
||||
type: InputFieldType.textInput,
|
||||
label: t('appDebug.variableConfig.defaultValue'),
|
||||
variable: 'default',
|
||||
placeholder: t('appDebug.variableConfig.defaultValuePlaceholder'),
|
||||
required: false,
|
||||
showConditions: [{
|
||||
variable: 'type',
|
||||
value: PipelineInputVarType.textInput,
|
||||
}],
|
||||
showOptional: true,
|
||||
}, {
|
||||
type: InputFieldType.textInput,
|
||||
label: t('appDebug.variableConfig.defaultValue'),
|
||||
variable: 'default',
|
||||
placeholder: t('appDebug.variableConfig.defaultValuePlaceholder'),
|
||||
required: false,
|
||||
showConditions: [{
|
||||
variable: 'type',
|
||||
value: PipelineInputVarType.paragraph,
|
||||
}],
|
||||
showOptional: true,
|
||||
}, {
|
||||
type: InputFieldType.numberInput,
|
||||
label: t('appDebug.variableConfig.defaultValue'),
|
||||
variable: 'default',
|
||||
placeholder: t('appDebug.variableConfig.defaultValuePlaceholder'),
|
||||
required: false,
|
||||
showConditions: [{
|
||||
variable: 'type',
|
||||
value: PipelineInputVarType.number,
|
||||
}],
|
||||
showOptional: true,
|
||||
}, {
|
||||
type: InputFieldType.select,
|
||||
label: t('appDebug.variableConfig.startSelectedOption'),
|
||||
variable: 'default',
|
||||
required: false,
|
||||
showConditions: [{
|
||||
variable: 'type',
|
||||
value: PipelineInputVarType.select,
|
||||
}],
|
||||
showOptional: true,
|
||||
options: defaultSelectOptions,
|
||||
popupProps: {
|
||||
wrapperClassName: 'z-40',
|
||||
},
|
||||
}, {
|
||||
type: InputFieldType.checkbox,
|
||||
label: t('appDebug.variableConfig.startChecked'),
|
||||
variable: 'default',
|
||||
required: false,
|
||||
showConditions: [{
|
||||
variable: 'type',
|
||||
value: PipelineInputVarType.checkbox,
|
||||
}],
|
||||
}, {
|
||||
type: InputFieldType.textInput,
|
||||
label: t('appDebug.variableConfig.placeholder'),
|
||||
variable: 'placeholder',
|
||||
placeholder: t('appDebug.variableConfig.placeholderPlaceholder'),
|
||||
required: false,
|
||||
showConditions: [{
|
||||
variable: 'type',
|
||||
value: PipelineInputVarType.textInput,
|
||||
}],
|
||||
showOptional: true,
|
||||
}, {
|
||||
type: InputFieldType.textInput,
|
||||
label: t('appDebug.variableConfig.placeholder'),
|
||||
variable: 'placeholder',
|
||||
placeholder: t('appDebug.variableConfig.placeholderPlaceholder'),
|
||||
required: false,
|
||||
showConditions: [{
|
||||
variable: 'type',
|
||||
value: PipelineInputVarType.paragraph,
|
||||
}],
|
||||
showOptional: true,
|
||||
}, {
|
||||
type: InputFieldType.textInput,
|
||||
label: t('appDebug.variableConfig.unit'),
|
||||
variable: 'unit',
|
||||
placeholder: t('appDebug.variableConfig.unitPlaceholder'),
|
||||
required: false,
|
||||
showConditions: [{
|
||||
variable: 'type',
|
||||
value: PipelineInputVarType.number,
|
||||
}],
|
||||
showOptional: true,
|
||||
}, {
|
||||
type: InputFieldType.textInput,
|
||||
label: t('appDebug.variableConfig.placeholder'),
|
||||
variable: 'placeholder',
|
||||
placeholder: t('appDebug.variableConfig.placeholderPlaceholder'),
|
||||
required: false,
|
||||
showConditions: [{
|
||||
variable: 'type',
|
||||
value: PipelineInputVarType.number,
|
||||
}],
|
||||
showOptional: true,
|
||||
}, {
|
||||
type: InputFieldType.uploadMethod,
|
||||
label: t('appDebug.variableConfig.uploadFileTypes'),
|
||||
variable: 'allowedFileUploadMethods',
|
||||
required: false,
|
||||
showConditions: [{
|
||||
variable: 'type',
|
||||
value: PipelineInputVarType.singleFile,
|
||||
}],
|
||||
}, {
|
||||
type: InputFieldType.uploadMethod,
|
||||
label: t('appDebug.variableConfig.uploadFileTypes'),
|
||||
variable: 'allowedFileUploadMethods',
|
||||
required: false,
|
||||
showConditions: [{
|
||||
variable: 'type',
|
||||
value: PipelineInputVarType.multiFiles,
|
||||
}],
|
||||
}, {
|
||||
type: InputFieldType.numberSlider,
|
||||
label: t('appDebug.variableConfig.maxNumberOfUploads'),
|
||||
variable: 'maxLength',
|
||||
required: false,
|
||||
showConditions: [{
|
||||
variable: 'type',
|
||||
value: PipelineInputVarType.multiFiles,
|
||||
}],
|
||||
description: t('appDebug.variableConfig.maxNumberTip', {
|
||||
imgLimit: formatFileSize(imgSizeLimit),
|
||||
docLimit: formatFileSize(docSizeLimit),
|
||||
audioLimit: formatFileSize(audioSizeLimit),
|
||||
videoLimit: formatFileSize(videoSizeLimit),
|
||||
}),
|
||||
}, {
|
||||
type: InputFieldType.textInput,
|
||||
label: t('appDebug.variableConfig.tooltips'),
|
||||
variable: 'tooltips',
|
||||
required: false,
|
||||
showConditions: [],
|
||||
showOptional: true,
|
||||
}]
|
||||
}, [defaultSelectOptions, imgSizeLimit, docSizeLimit, audioSizeLimit, videoSizeLimit, t])
|
||||
|
||||
return hiddenConfigurations
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useCallback, useState } from 'react'
|
||||
import type { MoreInfo } from '@/app/components/workflow/types'
|
||||
import { ChangeType } from '@/app/components/workflow/types'
|
||||
import { useFileUploadConfig } from '@/service/use-common'
|
||||
import type { FormData, InputFieldFormProps } from './types'
|
||||
import { useAppForm } from '@/app/components/base/form'
|
||||
import { createInputFieldSchema } from './schema'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { useFileSizeLimit } from '@/app/components/base/file-uploader/hooks'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import ShowAllSettings from './show-all-settings'
|
||||
import Button from '@/app/components/base/button'
|
||||
import InitialFields from './initial-fields'
|
||||
import HiddenFields from './hidden-fields'
|
||||
|
||||
const InputFieldForm = ({
|
||||
initialData,
|
||||
supportFile = false,
|
||||
onCancel,
|
||||
onSubmit,
|
||||
isEditMode = true,
|
||||
}: InputFieldFormProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { data: fileUploadConfigResponse } = useFileUploadConfig()
|
||||
const {
|
||||
maxFileUploadLimit,
|
||||
} = useFileSizeLimit(fileUploadConfigResponse)
|
||||
|
||||
const inputFieldForm = useAppForm({
|
||||
defaultValues: initialData,
|
||||
validators: {
|
||||
onSubmit: ({ value }) => {
|
||||
const { type } = value
|
||||
const schema = createInputFieldSchema(type, t, { maxFileUploadLimit })
|
||||
const result = schema.safeParse(value)
|
||||
if (!result.success) {
|
||||
const issues = result.error.issues
|
||||
const firstIssue = issues[0]
|
||||
const errorMessage = `"${firstIssue.path.join('.')}" ${firstIssue.message}`
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: errorMessage,
|
||||
})
|
||||
return errorMessage
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
},
|
||||
onSubmit: ({ value }) => {
|
||||
let moreInfo: MoreInfo | undefined
|
||||
if (isEditMode && value.variable !== initialData?.variable) {
|
||||
moreInfo = {
|
||||
type: ChangeType.changeVarName,
|
||||
payload: { beforeKey: initialData?.variable || '', afterKey: value.variable },
|
||||
}
|
||||
}
|
||||
onSubmit(value as FormData, moreInfo)
|
||||
},
|
||||
})
|
||||
|
||||
const [showAllSettings, setShowAllSettings] = useState(false)
|
||||
|
||||
const InitialFieldsComp = InitialFields({
|
||||
initialData,
|
||||
supportFile,
|
||||
})
|
||||
const HiddenFieldsComp = HiddenFields({
|
||||
initialData,
|
||||
})
|
||||
|
||||
const handleShowAllSettings = useCallback(() => {
|
||||
setShowAllSettings(true)
|
||||
}, [])
|
||||
|
||||
const ShowAllSettingComp = ShowAllSettings({
|
||||
initialData,
|
||||
handleShowAllSettings,
|
||||
})
|
||||
|
||||
return (
|
||||
<form
|
||||
className='w-full'
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
inputFieldForm.handleSubmit()
|
||||
}}
|
||||
>
|
||||
<div className='flex flex-col gap-4 px-4 py-2'>
|
||||
<InitialFieldsComp form={inputFieldForm} />
|
||||
<Divider type='horizontal' />
|
||||
{!showAllSettings && (
|
||||
<ShowAllSettingComp form={inputFieldForm} />
|
||||
)}
|
||||
{showAllSettings && (
|
||||
<HiddenFieldsComp form={inputFieldForm} />
|
||||
)}
|
||||
</div>
|
||||
<div className='flex items-center justify-end gap-x-2 p-4 pt-2'>
|
||||
<Button variant='secondary' onClick={onCancel}>
|
||||
{t('common.operation.cancel')}
|
||||
</Button>
|
||||
<inputFieldForm.AppForm>
|
||||
<inputFieldForm.Actions />
|
||||
</inputFieldForm.AppForm>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
export default InputFieldForm
|
||||
@@ -0,0 +1,47 @@
|
||||
import React, { useCallback } from 'react'
|
||||
import { withForm } from '@/app/components/base/form'
|
||||
import InputField from '@/app/components/base/form/form-scenarios/input-field/field'
|
||||
import { useConfigurations } from './hooks'
|
||||
|
||||
type InitialFieldsProps = {
|
||||
initialData?: Record<string, any>
|
||||
supportFile: boolean
|
||||
}
|
||||
|
||||
const InitialFields = ({
|
||||
initialData,
|
||||
supportFile,
|
||||
}: InitialFieldsProps) => withForm({
|
||||
defaultValues: initialData,
|
||||
render: function Render({
|
||||
form,
|
||||
}) {
|
||||
const getFieldValue = useCallback((fieldName: string) => {
|
||||
return form.getFieldValue(fieldName)
|
||||
}, [form])
|
||||
|
||||
const setFieldValue = useCallback((fieldName: string, value: any) => {
|
||||
form.setFieldValue(fieldName, value)
|
||||
}, [form])
|
||||
|
||||
const initialConfigurations = useConfigurations({
|
||||
getFieldValue,
|
||||
setFieldValue,
|
||||
supportFile,
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
{initialConfigurations.map((config, index) => {
|
||||
const FieldComponent = InputField({
|
||||
initialData,
|
||||
config,
|
||||
})
|
||||
return <FieldComponent key={index} form={form} />
|
||||
})}
|
||||
</>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export default InitialFields
|
||||
@@ -0,0 +1,89 @@
|
||||
import { MAX_VAR_KEY_LENGTH } from '@/config'
|
||||
import type { TFunction } from 'i18next'
|
||||
import { z } from 'zod'
|
||||
import type { SchemaOptions } from './types'
|
||||
import { PipelineInputVarType } from '@/models/pipeline'
|
||||
import { InputTypeEnum } from '@/app/components/base/form/components/field/input-type-select/types'
|
||||
|
||||
export const TEXT_MAX_LENGTH = 256
|
||||
|
||||
export const TransferMethod = z.enum([
|
||||
'all',
|
||||
'local_file',
|
||||
'remote_url',
|
||||
])
|
||||
|
||||
export const SupportedFileTypes = z.enum([
|
||||
'image',
|
||||
'document',
|
||||
'video',
|
||||
'audio',
|
||||
'custom',
|
||||
])
|
||||
|
||||
export const createInputFieldSchema = (type: PipelineInputVarType, t: TFunction, options: SchemaOptions) => {
|
||||
const { maxFileUploadLimit } = options
|
||||
const commonSchema = z.object({
|
||||
type: InputTypeEnum,
|
||||
variable: z.string().nonempty({
|
||||
message: t('appDebug.varKeyError.canNoBeEmpty', { key: t('appDebug.variableConfig.varName') }),
|
||||
}).max(MAX_VAR_KEY_LENGTH, {
|
||||
message: t('appDebug.varKeyError.tooLong', { key: t('appDebug.variableConfig.varName') }),
|
||||
}).regex(/^(?!\d)\w+/, {
|
||||
message: t('appDebug.varKeyError.notStartWithNumber', { key: t('appDebug.variableConfig.varName') }),
|
||||
}).regex(/^[a-zA-Z_]\w{0,29}$/, {
|
||||
message: t('appDebug.varKeyError.notValid', { key: t('appDebug.variableConfig.varName') }),
|
||||
}),
|
||||
label: z.string().nonempty({
|
||||
message: t('appDebug.variableConfig.errorMsg.labelNameRequired'),
|
||||
}),
|
||||
required: z.boolean(),
|
||||
tooltips: z.string().optional(),
|
||||
})
|
||||
if (type === PipelineInputVarType.textInput || type === PipelineInputVarType.paragraph) {
|
||||
return z.object({
|
||||
maxLength: z.number().min(1).max(TEXT_MAX_LENGTH),
|
||||
default: z.string().optional(),
|
||||
}).merge(commonSchema).passthrough()
|
||||
}
|
||||
if (type === PipelineInputVarType.number) {
|
||||
return z.object({
|
||||
default: z.number().optional(),
|
||||
unit: z.string().optional(),
|
||||
placeholder: z.string().optional(),
|
||||
}).merge(commonSchema).passthrough()
|
||||
}
|
||||
if (type === PipelineInputVarType.select) {
|
||||
return z.object({
|
||||
options: z.array(z.string()).nonempty({
|
||||
message: t('appDebug.variableConfig.errorMsg.atLeastOneOption'),
|
||||
}).refine(
|
||||
arr => new Set(arr).size === arr.length,
|
||||
{
|
||||
message: t('appDebug.variableConfig.errorMsg.optionRepeat'),
|
||||
},
|
||||
),
|
||||
default: z.string().optional(),
|
||||
}).merge(commonSchema).passthrough()
|
||||
}
|
||||
if (type === PipelineInputVarType.singleFile) {
|
||||
return z.object({
|
||||
allowedFileUploadMethods: z.array(TransferMethod),
|
||||
allowedTypesAndExtensions: z.object({
|
||||
allowedFileExtensions: z.array(z.string()).optional(),
|
||||
allowedFileTypes: z.array(SupportedFileTypes),
|
||||
}),
|
||||
}).merge(commonSchema).passthrough()
|
||||
}
|
||||
if (type === PipelineInputVarType.multiFiles) {
|
||||
return z.object({
|
||||
allowedFileUploadMethods: z.array(TransferMethod),
|
||||
allowedTypesAndExtensions: z.object({
|
||||
allowedFileExtensions: z.array(z.string()).optional(),
|
||||
allowedFileTypes: z.array(SupportedFileTypes),
|
||||
}),
|
||||
maxLength: z.number().min(1).max(maxFileUploadLimit),
|
||||
}).merge(commonSchema).passthrough()
|
||||
}
|
||||
return commonSchema.passthrough()
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import React from 'react'
|
||||
import { withForm } from '@/app/components/base/form'
|
||||
import { useStore } from '@tanstack/react-form'
|
||||
import { useHiddenFieldNames } from './hooks'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiArrowRightSLine } from '@remixicon/react'
|
||||
|
||||
type ShowAllSettingsProps = {
|
||||
initialData?: Record<string, any>
|
||||
handleShowAllSettings: () => void
|
||||
}
|
||||
|
||||
const ShowAllSettings = ({
|
||||
initialData,
|
||||
handleShowAllSettings,
|
||||
}: ShowAllSettingsProps) => withForm({
|
||||
defaultValues: initialData,
|
||||
render: function Render({
|
||||
form,
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const type = useStore(form.store, state => state.values.type)
|
||||
|
||||
const hiddenFieldNames = useHiddenFieldNames(type)
|
||||
|
||||
return (
|
||||
<div className='flex cursor-pointer items-center gap-x-4' onClick={handleShowAllSettings}>
|
||||
<div className='flex grow flex-col'>
|
||||
<span className='system-sm-medium flex min-h-6 items-center text-text-secondary'>
|
||||
{t('appDebug.variableConfig.showAllSettings')}
|
||||
</span>
|
||||
<span className='body-xs-regular pb-0.5 text-text-tertiary first-letter:capitalize'>
|
||||
{hiddenFieldNames}
|
||||
</span>
|
||||
</div>
|
||||
<RiArrowRightSLine className='h-4 w-4 shrink-0 text-text-secondary' />
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export default ShowAllSettings
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { MoreInfo, SupportUploadFileTypes } from '@/app/components/workflow/types'
|
||||
import type { PipelineInputVarType } from '@/models/pipeline'
|
||||
import type { TransferMethod } from '@/types/app'
|
||||
|
||||
export type FormData = {
|
||||
type: PipelineInputVarType
|
||||
label: string
|
||||
variable: string
|
||||
maxLength?: number
|
||||
default?: string
|
||||
required: boolean
|
||||
tooltips?: string
|
||||
options?: string[]
|
||||
placeholder?: string
|
||||
unit?: string
|
||||
allowedFileUploadMethods?: TransferMethod[]
|
||||
allowedTypesAndExtensions: {
|
||||
allowedFileTypes?: SupportUploadFileTypes[]
|
||||
allowedFileExtensions?: string[]
|
||||
}
|
||||
}
|
||||
|
||||
export type InputFieldFormProps = {
|
||||
initialData: Record<string, any>
|
||||
supportFile?: boolean
|
||||
onCancel: () => void
|
||||
onSubmit: (value: FormData, moreInfo?: MoreInfo) => void
|
||||
isEditMode?: boolean
|
||||
}
|
||||
|
||||
export type SchemaOptions = {
|
||||
maxFileUploadLimit: number
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import InputFieldForm from './form'
|
||||
import { convertFormDataToINputField, convertToInputFieldFormData } from './utils'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { InputVar } from '@/models/pipeline'
|
||||
import type { FormData } from './form/types'
|
||||
import type { MoreInfo } from '@/app/components/workflow/types'
|
||||
import { useFloatingRight } from '../hooks'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
export type InputFieldEditorProps = {
|
||||
onClose: () => void
|
||||
onSubmit: (data: InputVar, moreInfo?: MoreInfo) => void
|
||||
initialData?: InputVar
|
||||
}
|
||||
|
||||
const InputFieldEditorPanel = ({
|
||||
onClose,
|
||||
onSubmit,
|
||||
initialData,
|
||||
}: InputFieldEditorProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { floatingRight, floatingRightWidth } = useFloatingRight(400)
|
||||
|
||||
const formData = useMemo(() => {
|
||||
return convertToInputFieldFormData(initialData)
|
||||
}, [initialData])
|
||||
|
||||
const handleSubmit = useCallback((value: FormData, moreInfo?: MoreInfo) => {
|
||||
const inputFieldData = convertFormDataToINputField(value)
|
||||
onSubmit(inputFieldData, moreInfo)
|
||||
}, [onSubmit])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'relative mr-1 flex h-fit max-h-full w-[400px] flex-col overflow-y-auto rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-2xl shadow-shadow-shadow-9',
|
||||
'transition-all duration-300 ease-in-out',
|
||||
floatingRight && 'absolute right-0 z-[100]',
|
||||
)}
|
||||
style={{
|
||||
width: `${floatingRightWidth}px`,
|
||||
}}
|
||||
>
|
||||
<div className='system-xl-semibold flex items-center pb-1 pl-4 pr-11 pt-3.5 text-text-primary'>
|
||||
{initialData ? t('datasetPipeline.inputFieldPanel.editInputField') : t('datasetPipeline.inputFieldPanel.addInputField')}
|
||||
</div>
|
||||
<button
|
||||
type='button'
|
||||
className='absolute right-2.5 top-2.5 flex size-8 items-center justify-center'
|
||||
onClick={onClose}
|
||||
>
|
||||
<RiCloseLine className='size-4 text-text-tertiary' />
|
||||
</button>
|
||||
<InputFieldForm
|
||||
initialData={formData}
|
||||
supportFile
|
||||
onCancel={onClose}
|
||||
onSubmit={handleSubmit}
|
||||
isEditMode={!!initialData}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default InputFieldEditorPanel
|
||||
@@ -0,0 +1,89 @@
|
||||
import type { InputVar } from '@/models/pipeline'
|
||||
import type { FormData } from './form/types'
|
||||
import { VAR_ITEM_TEMPLATE_IN_PIPELINE } from '@/config'
|
||||
|
||||
const getNewInputVarInRagPipeline = (): InputVar => {
|
||||
return {
|
||||
...VAR_ITEM_TEMPLATE_IN_PIPELINE,
|
||||
}
|
||||
}
|
||||
|
||||
export const convertToInputFieldFormData = (data?: InputVar): FormData => {
|
||||
const {
|
||||
type,
|
||||
label,
|
||||
variable,
|
||||
max_length,
|
||||
default_value,
|
||||
required,
|
||||
tooltips,
|
||||
options,
|
||||
placeholder,
|
||||
unit,
|
||||
allowed_file_upload_methods,
|
||||
allowed_file_types,
|
||||
allowed_file_extensions,
|
||||
} = data || getNewInputVarInRagPipeline()
|
||||
|
||||
const formData: FormData = {
|
||||
type,
|
||||
label,
|
||||
variable,
|
||||
maxLength: max_length,
|
||||
required,
|
||||
options,
|
||||
allowedTypesAndExtensions: {},
|
||||
}
|
||||
|
||||
if (default_value !== undefined && default_value !== null)
|
||||
formData.default = default_value
|
||||
if (tooltips !== undefined && tooltips !== null)
|
||||
formData.tooltips = tooltips
|
||||
if (placeholder !== undefined && placeholder !== null)
|
||||
formData.placeholder = placeholder
|
||||
if (unit !== undefined && unit !== null)
|
||||
formData.unit = unit
|
||||
if (allowed_file_upload_methods)
|
||||
formData.allowedFileUploadMethods = allowed_file_upload_methods
|
||||
if (allowed_file_types && allowed_file_extensions) {
|
||||
formData.allowedTypesAndExtensions = {
|
||||
allowedFileTypes: allowed_file_types,
|
||||
allowedFileExtensions: allowed_file_extensions,
|
||||
}
|
||||
}
|
||||
|
||||
return formData
|
||||
}
|
||||
|
||||
export const convertFormDataToINputField = (data: FormData): InputVar => {
|
||||
const {
|
||||
type,
|
||||
label,
|
||||
variable,
|
||||
maxLength,
|
||||
default: defaultValue,
|
||||
required,
|
||||
tooltips,
|
||||
options,
|
||||
placeholder,
|
||||
unit,
|
||||
allowedFileUploadMethods,
|
||||
allowedTypesAndExtensions: { allowedFileTypes, allowedFileExtensions },
|
||||
} = data
|
||||
|
||||
return {
|
||||
type,
|
||||
label,
|
||||
variable,
|
||||
max_length: maxLength,
|
||||
default_value: defaultValue,
|
||||
required,
|
||||
tooltips,
|
||||
options,
|
||||
placeholder,
|
||||
unit,
|
||||
allowed_file_upload_methods: allowedFileUploadMethods,
|
||||
allowed_file_types: allowedFileTypes,
|
||||
allowed_file_extensions: allowedFileExtensions,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
'use client'
|
||||
import React, { useCallback, useRef } from 'react'
|
||||
import { useHover } from 'ahooks'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiDeleteBinLine,
|
||||
RiDraggable,
|
||||
RiEditLine,
|
||||
} from '@remixicon/react'
|
||||
import { InputField } from '@/app/components/base/icons/src/vender/pipeline'
|
||||
import InputVarTypeIcon from '@/app/components/workflow/nodes/_base/components/input-var-type-icon'
|
||||
import cn from '@/utils/classnames'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
import type { InputVar } from '@/models/pipeline'
|
||||
import type { InputVarType } from '@/app/components/workflow/types'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
|
||||
type FieldItemProps = {
|
||||
readonly?: boolean
|
||||
payload: InputVar
|
||||
index: number
|
||||
onClickEdit: (id: string) => void
|
||||
onRemove: (index: number) => void
|
||||
}
|
||||
|
||||
const FieldItem = ({
|
||||
readonly,
|
||||
payload,
|
||||
index,
|
||||
onClickEdit,
|
||||
onRemove,
|
||||
}: FieldItemProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const ref = useRef(null)
|
||||
const isHovering = useHover(ref)
|
||||
|
||||
const handleOnClickEdit = useCallback((e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
if (readonly) return
|
||||
onClickEdit(payload.variable)
|
||||
}, [onClickEdit, payload.variable, readonly])
|
||||
|
||||
const handleRemove = useCallback((e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
if (readonly) return
|
||||
onRemove(index)
|
||||
}, [index, onRemove, readonly])
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'handle flex h-8 cursor-pointer items-center justify-between gap-x-1 rounded-lg border border-components-panel-border-subtle bg-components-panel-on-panel-item-bg py-1 pl-2 shadow-xs hover:shadow-sm',
|
||||
(isHovering && !readonly) ? 'cursor-all-scroll pr-1' : 'pr-2.5',
|
||||
readonly && 'cursor-default',
|
||||
)}
|
||||
// onClick={handleOnClickEdit}
|
||||
>
|
||||
<div className='flex grow basis-0 items-center gap-x-1 overflow-hidden'>
|
||||
{
|
||||
(isHovering && !readonly)
|
||||
? <RiDraggable className='size-4 shrink-0 text-text-quaternary' />
|
||||
: <InputField className='size-4 shrink-0 text-text-accent' />
|
||||
}
|
||||
<div
|
||||
title={payload.variable}
|
||||
className='system-sm-medium max-w-[130px] shrink-0 truncate text-text-secondary'
|
||||
>
|
||||
{payload.variable}
|
||||
</div>
|
||||
{payload.label && (
|
||||
<>
|
||||
<div className='system-xs-regular shrink-0 text-text-quaternary'>·</div>
|
||||
<div
|
||||
title={payload.label}
|
||||
className='system-xs-medium grow truncate text-text-tertiary'
|
||||
>
|
||||
{payload.label}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{(isHovering && !readonly)
|
||||
? (
|
||||
<div className='flex shrink-0 items-center gap-x-1'>
|
||||
<ActionButton
|
||||
className='mr-1'
|
||||
onClick={handleOnClickEdit}
|
||||
>
|
||||
<RiEditLine className='size-4 text-text-tertiary' />
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
onClick={handleRemove}
|
||||
>
|
||||
<RiDeleteBinLine className='size-4 text-text-tertiary group-hover:text-text-destructive' />
|
||||
</ActionButton>
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div className='flex shrink-0 items-center gap-x-2'>
|
||||
{payload.required && (
|
||||
<Badge>{t('workflow.nodes.start.required')}</Badge>
|
||||
)}
|
||||
<InputVarTypeIcon type={payload.type as unknown as InputVarType} className='h-3 w-3 text-text-tertiary' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(FieldItem)
|
||||
@@ -0,0 +1,70 @@
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import { ReactSortable } from 'react-sortablejs'
|
||||
import cn from '@/utils/classnames'
|
||||
import type { InputVar } from '@/models/pipeline'
|
||||
import FieldItem from './field-item'
|
||||
import type { SortableItem } from './types'
|
||||
import { isEqual } from 'lodash-es'
|
||||
|
||||
type FieldListContainerProps = {
|
||||
className?: string
|
||||
inputFields: InputVar[]
|
||||
onListSortChange: (list: SortableItem[]) => void
|
||||
onRemoveField: (index: number) => void
|
||||
onEditField: (id: string) => void
|
||||
readonly?: boolean
|
||||
}
|
||||
const FieldListContainer = ({
|
||||
className,
|
||||
inputFields,
|
||||
onListSortChange,
|
||||
onRemoveField,
|
||||
onEditField,
|
||||
readonly,
|
||||
}: FieldListContainerProps) => {
|
||||
const list = useMemo(() => {
|
||||
return inputFields.map((content) => {
|
||||
return ({
|
||||
id: content.variable,
|
||||
chosen: false,
|
||||
selected: false,
|
||||
...content,
|
||||
})
|
||||
})
|
||||
}, [inputFields])
|
||||
|
||||
const handleListSortChange = useCallback((newList: SortableItem[]) => {
|
||||
if (isEqual(newList, list))
|
||||
return
|
||||
onListSortChange(newList)
|
||||
}, [list, onListSortChange])
|
||||
|
||||
return (
|
||||
<ReactSortable<SortableItem>
|
||||
className={cn(className)}
|
||||
list={list}
|
||||
setList={handleListSortChange}
|
||||
handle='.handle'
|
||||
ghostClass='opacity-50'
|
||||
animation={150}
|
||||
disabled={readonly}
|
||||
>
|
||||
{inputFields?.map((item, index) => (
|
||||
<FieldItem
|
||||
key={index}
|
||||
index={index}
|
||||
readonly={readonly}
|
||||
payload={item}
|
||||
onRemove={onRemoveField}
|
||||
onClickEdit={onEditField}
|
||||
/>
|
||||
))}
|
||||
</ReactSortable>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(FieldListContainer)
|
||||
@@ -0,0 +1,131 @@
|
||||
import {
|
||||
useCallback,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { produce } from 'immer'
|
||||
import type { InputVar } from '@/models/pipeline'
|
||||
import type { SortableItem } from './types'
|
||||
import type { MoreInfo, ValueSelector } from '@/app/components/workflow/types'
|
||||
import { ChangeType } from '@/app/components/workflow/types'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { usePipeline } from '../../../../hooks/use-pipeline'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useInputFieldPanel } from '@/app/components/rag-pipeline/hooks'
|
||||
|
||||
const VARIABLE_PREFIX = 'rag'
|
||||
|
||||
type useFieldListProps = {
|
||||
initialInputFields: InputVar[],
|
||||
onInputFieldsChange: (value: InputVar[]) => void,
|
||||
nodeId: string,
|
||||
allVariableNames: string[],
|
||||
}
|
||||
|
||||
export const useFieldList = ({
|
||||
initialInputFields,
|
||||
onInputFieldsChange,
|
||||
nodeId,
|
||||
allVariableNames,
|
||||
}: useFieldListProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { toggleInputFieldEditPanel } = useInputFieldPanel()
|
||||
const [inputFields, setInputFields] = useState<InputVar[]>(initialInputFields)
|
||||
const inputFieldsRef = useRef<InputVar[]>(inputFields)
|
||||
const [removedVar, setRemovedVar] = useState<ValueSelector>([])
|
||||
const [removedIndex, setRemoveIndex] = useState(0)
|
||||
|
||||
const { handleInputVarRename, isVarUsedInNodes, removeUsedVarInNodes } = usePipeline()
|
||||
|
||||
const [isShowRemoveVarConfirm, {
|
||||
setTrue: showRemoveVarConfirm,
|
||||
setFalse: hideRemoveVarConfirm,
|
||||
}] = useBoolean(false)
|
||||
|
||||
const handleInputFieldsChange = useCallback((newInputFields: InputVar[]) => {
|
||||
setInputFields(newInputFields)
|
||||
inputFieldsRef.current = newInputFields
|
||||
onInputFieldsChange(newInputFields)
|
||||
}, [onInputFieldsChange])
|
||||
|
||||
const handleListSortChange = useCallback((list: SortableItem[]) => {
|
||||
const newInputFields = list.map((item) => {
|
||||
const { id: _id, chosen: _chosen, selected: _selected, ...filed } = item
|
||||
return filed
|
||||
})
|
||||
handleInputFieldsChange(newInputFields)
|
||||
}, [handleInputFieldsChange])
|
||||
|
||||
const editingFieldIndex = useRef<number>(-1)
|
||||
|
||||
const handleCloseInputFieldEditor = useCallback(() => {
|
||||
toggleInputFieldEditPanel?.(null)
|
||||
editingFieldIndex.current = -1
|
||||
}, [toggleInputFieldEditPanel])
|
||||
|
||||
const handleRemoveField = useCallback((index: number) => {
|
||||
const itemToRemove = inputFieldsRef.current[index]
|
||||
// Check if the variable is used in other nodes
|
||||
if (isVarUsedInNodes([VARIABLE_PREFIX, nodeId, itemToRemove.variable || ''])) {
|
||||
showRemoveVarConfirm()
|
||||
setRemovedVar([VARIABLE_PREFIX, nodeId, itemToRemove.variable || ''])
|
||||
setRemoveIndex(index as number)
|
||||
return
|
||||
}
|
||||
const newInputFields = inputFieldsRef.current.filter((_, i) => i !== index)
|
||||
handleInputFieldsChange(newInputFields)
|
||||
}, [handleInputFieldsChange, isVarUsedInNodes, nodeId, showRemoveVarConfirm])
|
||||
|
||||
const onRemoveVarConfirm = useCallback(() => {
|
||||
const newInputFields = inputFieldsRef.current.filter((_, i) => i !== removedIndex)
|
||||
handleInputFieldsChange(newInputFields)
|
||||
removeUsedVarInNodes(removedVar)
|
||||
hideRemoveVarConfirm()
|
||||
}, [removedIndex, handleInputFieldsChange, removeUsedVarInNodes, removedVar, hideRemoveVarConfirm])
|
||||
|
||||
const handleSubmitField = useCallback((data: InputVar, moreInfo?: MoreInfo) => {
|
||||
const isDuplicate = allVariableNames.some(name =>
|
||||
name === data.variable && name !== inputFieldsRef.current[editingFieldIndex.current]?.variable)
|
||||
if (isDuplicate) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('datasetPipeline.inputFieldPanel.error.variableDuplicate'),
|
||||
})
|
||||
return
|
||||
}
|
||||
const newInputFields = produce(inputFieldsRef.current, (draft) => {
|
||||
const currentIndex = editingFieldIndex.current
|
||||
if (currentIndex === -1) {
|
||||
draft.push(data)
|
||||
return
|
||||
}
|
||||
draft[currentIndex] = data
|
||||
})
|
||||
handleInputFieldsChange(newInputFields)
|
||||
// Update variable name in nodes if it has changed
|
||||
if (moreInfo?.type === ChangeType.changeVarName)
|
||||
handleInputVarRename(nodeId, [VARIABLE_PREFIX, nodeId, moreInfo.payload?.beforeKey || ''], [VARIABLE_PREFIX, nodeId, moreInfo.payload?.afterKey || ''])
|
||||
handleCloseInputFieldEditor()
|
||||
}, [allVariableNames, handleCloseInputFieldEditor, handleInputFieldsChange, handleInputVarRename, nodeId, t])
|
||||
|
||||
const handleOpenInputFieldEditor = useCallback((id?: string) => {
|
||||
const index = inputFieldsRef.current.findIndex(field => field.variable === id)
|
||||
editingFieldIndex.current = index
|
||||
toggleInputFieldEditPanel?.({
|
||||
onClose: handleCloseInputFieldEditor,
|
||||
onSubmit: handleSubmitField,
|
||||
initialData: inputFieldsRef.current[index],
|
||||
})
|
||||
}, [])
|
||||
|
||||
return {
|
||||
inputFields,
|
||||
handleListSortChange,
|
||||
handleRemoveField,
|
||||
handleOpenInputFieldEditor,
|
||||
isShowRemoveVarConfirm,
|
||||
hideRemoveVarConfirm,
|
||||
onRemoveVarConfirm,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import React, { useCallback } from 'react'
|
||||
import { RiAddLine } from '@remixicon/react'
|
||||
import cn from '@/utils/classnames'
|
||||
import type { InputVar } from '@/models/pipeline'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import { useFieldList } from './hooks'
|
||||
import FieldListContainer from './field-list-container'
|
||||
import RemoveEffectVarConfirm from '@/app/components/workflow/nodes/_base/components/remove-effect-var-confirm'
|
||||
|
||||
type FieldListProps = {
|
||||
nodeId: string
|
||||
LabelRightContent: React.ReactNode
|
||||
inputFields: InputVar[]
|
||||
handleInputFieldsChange: (key: string, value: InputVar[]) => void
|
||||
readonly?: boolean
|
||||
labelClassName?: string
|
||||
allVariableNames: string[]
|
||||
}
|
||||
|
||||
const FieldList = ({
|
||||
nodeId,
|
||||
LabelRightContent,
|
||||
inputFields: initialInputFields,
|
||||
handleInputFieldsChange,
|
||||
readonly,
|
||||
labelClassName,
|
||||
allVariableNames,
|
||||
}: FieldListProps) => {
|
||||
const onInputFieldsChange = useCallback((value: InputVar[]) => {
|
||||
handleInputFieldsChange(nodeId, value)
|
||||
}, [handleInputFieldsChange, nodeId])
|
||||
|
||||
const {
|
||||
inputFields,
|
||||
handleListSortChange,
|
||||
handleRemoveField,
|
||||
handleOpenInputFieldEditor,
|
||||
isShowRemoveVarConfirm,
|
||||
hideRemoveVarConfirm,
|
||||
onRemoveVarConfirm,
|
||||
} = useFieldList({
|
||||
initialInputFields,
|
||||
onInputFieldsChange,
|
||||
nodeId,
|
||||
allVariableNames,
|
||||
})
|
||||
|
||||
return (
|
||||
<div className='flex flex-col'>
|
||||
<div className={cn('flex items-center gap-x-2 px-4', labelClassName)}>
|
||||
<div className='grow'>
|
||||
{LabelRightContent}
|
||||
</div>
|
||||
<ActionButton
|
||||
onClick={() => handleOpenInputFieldEditor()}
|
||||
disabled={readonly}
|
||||
className={cn(readonly && 'cursor-not-allowed')}
|
||||
>
|
||||
<RiAddLine className='h-4 w-4 text-text-tertiary' />
|
||||
</ActionButton>
|
||||
</div>
|
||||
<FieldListContainer
|
||||
className='flex flex-col gap-y-1 px-4 pb-1'
|
||||
inputFields={inputFields}
|
||||
onEditField={handleOpenInputFieldEditor}
|
||||
onRemoveField={handleRemoveField}
|
||||
onListSortChange={handleListSortChange}
|
||||
readonly={readonly}
|
||||
/>
|
||||
<RemoveEffectVarConfirm
|
||||
isShow={isShowRemoveVarConfirm}
|
||||
onCancel={hideRemoveVarConfirm}
|
||||
onConfirm={onRemoveVarConfirm}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(FieldList)
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { InputVar } from '@/models/pipeline'
|
||||
|
||||
export type SortableItem = {
|
||||
id: string
|
||||
chosen: boolean,
|
||||
selected: boolean,
|
||||
} & InputVar
|
||||
@@ -0,0 +1,13 @@
|
||||
import React from 'react'
|
||||
import { RiDragDropLine } from '@remixicon/react'
|
||||
|
||||
const FooterTip = () => {
|
||||
return (
|
||||
<div className='flex shrink-0 items-center justify-center gap-x-2 py-4 text-text-quaternary'>
|
||||
<RiDragDropLine className='size-4' />
|
||||
<span className='system-xs-regular'>Drag to adjust grouping</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(FooterTip)
|
||||
@@ -0,0 +1,39 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import { useStore as useReactflow } from 'reactflow'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
|
||||
export const useFloatingRight = (targetElementWidth: number) => {
|
||||
const [floatingRight, setFloatingRight] = useState(false)
|
||||
const nodePanelWidth = useStore(state => state.nodePanelWidth)
|
||||
const workflowCanvasWidth = useStore(state => state.workflowCanvasWidth)
|
||||
const otherPanelWidth = useStore(state => state.otherPanelWidth)
|
||||
|
||||
const selectedNodeId = useReactflow(useShallow((s) => {
|
||||
const nodes = s.getNodes()
|
||||
const currentNode = nodes.find(node => node.data.selected)
|
||||
|
||||
if (currentNode)
|
||||
return currentNode.id
|
||||
}))
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof workflowCanvasWidth === 'number') {
|
||||
const inputFieldPanelWidth = 400
|
||||
const marginRight = 4
|
||||
const leftWidth = workflowCanvasWidth - (selectedNodeId ? nodePanelWidth : 0) - otherPanelWidth - inputFieldPanelWidth - marginRight
|
||||
setFloatingRight(leftWidth < targetElementWidth + marginRight)
|
||||
}
|
||||
}, [workflowCanvasWidth, nodePanelWidth, otherPanelWidth, selectedNodeId, targetElementWidth])
|
||||
|
||||
const floatingRightWidth = useMemo(() => {
|
||||
if (!floatingRight) return targetElementWidth
|
||||
const width = Math.min(targetElementWidth, (selectedNodeId ? nodePanelWidth : 0) + otherPanelWidth)
|
||||
return width
|
||||
}, [floatingRight, selectedNodeId, nodePanelWidth, otherPanelWidth, targetElementWidth])
|
||||
|
||||
return {
|
||||
floatingRight,
|
||||
floatingRightWidth,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from 'react'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import { RiCloseLine, RiEyeLine } from '@remixicon/react'
|
||||
import type { Node } from '@/app/components/workflow/types'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import FieldList from './field-list'
|
||||
import FooterTip from './footer-tip'
|
||||
import GlobalInputs from './label-right-content/global-inputs'
|
||||
import Datasource from './label-right-content/datasource'
|
||||
import { useNodes } from 'reactflow'
|
||||
import type { DataSourceNodeType } from '@/app/components/workflow/nodes/data-source/types'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useNodesSyncDraft } from '@/app/components/workflow/hooks'
|
||||
import type { InputVar, RAGPipelineVariables } from '@/models/pipeline'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import cn from '@/utils/classnames'
|
||||
import { useInputFieldPanel } from '@/app/components/rag-pipeline/hooks'
|
||||
|
||||
const InputFieldPanel = () => {
|
||||
const { t } = useTranslation()
|
||||
const nodes = useNodes<DataSourceNodeType>()
|
||||
const {
|
||||
closeAllInputFieldPanels,
|
||||
toggleInputFieldPreviewPanel,
|
||||
isPreviewing,
|
||||
isEditing,
|
||||
} = useInputFieldPanel()
|
||||
const ragPipelineVariables = useStore(state => state.ragPipelineVariables)
|
||||
const setRagPipelineVariables = useStore(state => state.setRagPipelineVariables)
|
||||
|
||||
const getInputFieldsMap = () => {
|
||||
const inputFieldsMap: Record<string, InputVar[]> = {}
|
||||
ragPipelineVariables?.forEach((variable) => {
|
||||
const { belong_to_node_id: nodeId, ...varConfig } = variable
|
||||
if (inputFieldsMap[nodeId])
|
||||
inputFieldsMap[nodeId].push(varConfig)
|
||||
else
|
||||
inputFieldsMap[nodeId] = [varConfig]
|
||||
})
|
||||
return inputFieldsMap
|
||||
}
|
||||
const inputFieldsMap = useRef(getInputFieldsMap())
|
||||
|
||||
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
|
||||
const datasourceNodeDataMap = useMemo(() => {
|
||||
const datasourceNodeDataMap: Record<string, DataSourceNodeType> = {}
|
||||
const datasourceNodes: Node<DataSourceNodeType>[] = nodes.filter(node => node.data.type === BlockEnum.DataSource)
|
||||
datasourceNodes.forEach((node) => {
|
||||
const { id, data } = node
|
||||
datasourceNodeDataMap[id] = data
|
||||
})
|
||||
return datasourceNodeDataMap
|
||||
}, [nodes])
|
||||
|
||||
const updateInputFields = useCallback(async (key: string, value: InputVar[]) => {
|
||||
inputFieldsMap.current[key] = value
|
||||
const datasourceNodeInputFields: RAGPipelineVariables = []
|
||||
const globalInputFields: RAGPipelineVariables = []
|
||||
Object.keys(inputFieldsMap.current).forEach((key) => {
|
||||
const inputFields = inputFieldsMap.current[key]
|
||||
inputFields.forEach((inputField) => {
|
||||
if (key === 'shared') {
|
||||
globalInputFields.push({
|
||||
...inputField,
|
||||
belong_to_node_id: key,
|
||||
})
|
||||
}
|
||||
else {
|
||||
datasourceNodeInputFields.push({
|
||||
...inputField,
|
||||
belong_to_node_id: key,
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
// Datasource node input fields come first, then global input fields
|
||||
const newRagPipelineVariables = [...datasourceNodeInputFields, ...globalInputFields]
|
||||
setRagPipelineVariables?.(newRagPipelineVariables)
|
||||
handleSyncWorkflowDraft()
|
||||
}, [setRagPipelineVariables, handleSyncWorkflowDraft])
|
||||
|
||||
const closePanel = useCallback(() => {
|
||||
closeAllInputFieldPanels()
|
||||
}, [closeAllInputFieldPanels])
|
||||
|
||||
const togglePreviewPanel = useCallback(() => {
|
||||
toggleInputFieldPreviewPanel()
|
||||
}, [toggleInputFieldPreviewPanel])
|
||||
|
||||
const allVariableNames = useMemo(() => {
|
||||
return ragPipelineVariables?.map(variable => variable.variable) || []
|
||||
}, [ragPipelineVariables])
|
||||
|
||||
return (
|
||||
<div className='mr-1 flex h-full w-[400px] flex-col rounded-2xl border-y-[0.5px] border-l-[0.5px] border-components-panel-border bg-components-panel-bg-alt shadow-xl shadow-shadow-shadow-5'>
|
||||
<div className='flex shrink-0 items-center p-4 pb-0'>
|
||||
<div className='system-xl-semibold grow text-text-primary'>
|
||||
{t('datasetPipeline.inputFieldPanel.title')}
|
||||
</div>
|
||||
<Button
|
||||
variant={'ghost'}
|
||||
size='small'
|
||||
className={cn(
|
||||
'shrink-0 gap-x-px px-1.5',
|
||||
isPreviewing && 'bg-state-accent-active text-text-accent',
|
||||
)}
|
||||
onClick={togglePreviewPanel}
|
||||
disabled={isEditing}
|
||||
>
|
||||
<RiEyeLine className='size-3.5' />
|
||||
<span className='px-[3px]'>{t('datasetPipeline.operations.preview')}</span>
|
||||
</Button>
|
||||
<Divider type='vertical' className='mx-1 h-3' />
|
||||
<button
|
||||
type='button'
|
||||
className='flex size-6 shrink-0 items-center justify-center p-0.5'
|
||||
onClick={closePanel}
|
||||
>
|
||||
<RiCloseLine className='size-4 text-text-tertiary' />
|
||||
</button>
|
||||
</div>
|
||||
<div className='system-sm-regular shrink-0 px-4 pb-2 pt-1 text-text-tertiary'>
|
||||
{t('datasetPipeline.inputFieldPanel.description')}
|
||||
</div>
|
||||
<div className='flex grow flex-col overflow-y-auto'>
|
||||
{/* Unique Inputs for Each Entrance */}
|
||||
<div className='flex h-6 items-center gap-x-0.5 px-4 pt-2'>
|
||||
<span className='system-sm-semibold-uppercase text-text-secondary'>
|
||||
{t('datasetPipeline.inputFieldPanel.uniqueInputs.title')}
|
||||
</span>
|
||||
<Tooltip
|
||||
popupContent={t('datasetPipeline.inputFieldPanel.uniqueInputs.tooltip')}
|
||||
popupClassName='max-w-[240px]'
|
||||
/>
|
||||
</div>
|
||||
<div className='flex flex-col gap-y-1 py-1'>
|
||||
{
|
||||
Object.keys(datasourceNodeDataMap).map((key) => {
|
||||
const inputFields = inputFieldsMap.current[key] || []
|
||||
return (
|
||||
<FieldList
|
||||
key={key}
|
||||
nodeId={key}
|
||||
LabelRightContent={<Datasource nodeData={datasourceNodeDataMap[key]} />}
|
||||
inputFields={inputFields}
|
||||
readonly={isPreviewing || isEditing}
|
||||
labelClassName='pt-1 pb-1'
|
||||
handleInputFieldsChange={updateInputFields}
|
||||
allVariableNames={allVariableNames}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
{/* Global Inputs */}
|
||||
<FieldList
|
||||
nodeId='shared'
|
||||
LabelRightContent={<GlobalInputs />}
|
||||
inputFields={inputFieldsMap.current.shared || []}
|
||||
readonly={isPreviewing || isEditing}
|
||||
labelClassName='pt-2 pb-1'
|
||||
handleInputFieldsChange={updateInputFields}
|
||||
allVariableNames={allVariableNames}
|
||||
/>
|
||||
</div>
|
||||
<FooterTip />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(InputFieldPanel)
|
||||
@@ -0,0 +1,30 @@
|
||||
import React from 'react'
|
||||
import type { DataSourceNodeType } from '@/app/components/workflow/nodes/data-source/types'
|
||||
import { useToolIcon } from '@/app/components/workflow/hooks'
|
||||
import BlockIcon from '@/app/components/workflow/block-icon'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
|
||||
type DatasourceProps = {
|
||||
nodeData: DataSourceNodeType
|
||||
}
|
||||
|
||||
const Datasource = ({
|
||||
nodeData,
|
||||
}: DatasourceProps) => {
|
||||
const toolIcon = useToolIcon(nodeData)
|
||||
|
||||
return (
|
||||
<div className='flex items-center gap-x-1.5'>
|
||||
<div className='flex size-5 items-center justify-center rounded-md border-[0.5px] border-components-panel-border-subtle bg-background-default'>
|
||||
<BlockIcon
|
||||
className='size-3.5'
|
||||
type={BlockEnum.DataSource}
|
||||
toolIcon={toolIcon}
|
||||
/>
|
||||
</div>
|
||||
<span className='system-sm-medium text-text-secondary'>{nodeData.title}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Datasource)
|
||||
@@ -0,0 +1,21 @@
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const GlobalInputs = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className='flex items-center gap-x-1'>
|
||||
<span className='system-sm-semibold-uppercase text-text-secondary'>
|
||||
{t('datasetPipeline.inputFieldPanel.globalInputs.title')}
|
||||
</span>
|
||||
<Tooltip
|
||||
popupContent={t('datasetPipeline.inputFieldPanel.globalInputs.tooltip')}
|
||||
popupClassName='w-[240px]'
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(GlobalInputs)
|
||||
@@ -0,0 +1,41 @@
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
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'
|
||||
import { useDraftPipelinePreProcessingParams } from '@/service/use-pipeline'
|
||||
|
||||
type DatasourceProps = {
|
||||
onSelect: (dataSource: Datasource) => void
|
||||
dataSourceNodeId: string
|
||||
}
|
||||
|
||||
const DataSource = ({
|
||||
onSelect: setDatasource,
|
||||
dataSourceNodeId,
|
||||
}: DatasourceProps) => {
|
||||
const { t } = useTranslation()
|
||||
const pipelineId = useStore(state => state.pipelineId)
|
||||
const { data: paramsConfig } = useDraftPipelinePreProcessingParams({
|
||||
pipeline_id: pipelineId!,
|
||||
node_id: dataSourceNodeId,
|
||||
}, !!pipelineId && !!dataSourceNodeId)
|
||||
|
||||
return (
|
||||
<div className='flex flex-col'>
|
||||
<div className='system-sm-semibold-uppercase px-4 pt-2 text-text-secondary'>
|
||||
{t('datasetPipeline.inputFieldPanel.preview.stepOneTitle')}
|
||||
</div>
|
||||
<div className='px-4 py-2'>
|
||||
<DataSourceOptions
|
||||
onSelect={setDatasource}
|
||||
dataSourceNodeId={dataSourceNodeId}
|
||||
/>
|
||||
</div>
|
||||
<Form variables={paramsConfig?.variables || []} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(DataSource)
|
||||
@@ -0,0 +1,41 @@
|
||||
import { useAppForm } from '@/app/components/base/form'
|
||||
import BaseField from '@/app/components/base/form/form-scenarios/base/field'
|
||||
import type { RAGPipelineVariables } from '@/models/pipeline'
|
||||
import { useConfigurations, useInitialData } from '@/app/components/rag-pipeline/hooks/use-input-fields'
|
||||
|
||||
type FormProps = {
|
||||
variables: RAGPipelineVariables
|
||||
}
|
||||
|
||||
const Form = ({
|
||||
variables,
|
||||
}: FormProps) => {
|
||||
const initialData = useInitialData(variables)
|
||||
const configurations = useConfigurations(variables)
|
||||
|
||||
const form = useAppForm({
|
||||
defaultValues: initialData,
|
||||
})
|
||||
|
||||
return (
|
||||
<form
|
||||
className='w-full'
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}}
|
||||
>
|
||||
<div className='flex flex-col gap-y-3 px-4 py-3'>
|
||||
{configurations.map((config, index) => {
|
||||
const FieldComponent = BaseField({
|
||||
initialData,
|
||||
config,
|
||||
})
|
||||
return <FieldComponent key={index} form={form} />
|
||||
})}
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
export default Form
|
||||
@@ -0,0 +1,63 @@
|
||||
import { useCallback, useState } from 'react'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
import DataSource from './data-source'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import ProcessDocuments from './process-documents'
|
||||
import type { Datasource } from '../../test-run/types'
|
||||
import { useInputFieldPanel } from '@/app/components/rag-pipeline/hooks'
|
||||
import cn from '@/utils/classnames'
|
||||
import { useFloatingRight } from '../hooks'
|
||||
|
||||
const PreviewPanel = () => {
|
||||
const { t } = useTranslation()
|
||||
const [datasource, setDatasource] = useState<Datasource>()
|
||||
const { toggleInputFieldPreviewPanel } = useInputFieldPanel()
|
||||
|
||||
const { floatingRight, floatingRightWidth } = useFloatingRight(480)
|
||||
|
||||
const handleClosePreviewPanel = useCallback(() => {
|
||||
toggleInputFieldPreviewPanel()
|
||||
}, [toggleInputFieldPreviewPanel])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'mr-1 flex h-full flex-col overflow-y-auto rounded-2xl border-y-[0.5px] border-l-[0.5px] border-components-panel-border bg-components-panel-bg shadow-xl shadow-shadow-shadow-5',
|
||||
'transition-all duration-300 ease-in-out',
|
||||
floatingRight && 'absolute right-0 z-[100]',
|
||||
)}
|
||||
style={{
|
||||
width: `${floatingRightWidth}px`,
|
||||
}}
|
||||
>
|
||||
<div className='flex items-center gap-x-2 px-4 pt-1'>
|
||||
<div className='grow py-1'>
|
||||
<Badge className='border-text-accent-secondary bg-components-badge-bg-dimm text-text-accent-secondary'>
|
||||
{t('datasetPipeline.operations.preview')}
|
||||
</Badge>
|
||||
</div>
|
||||
<button
|
||||
type='button'
|
||||
className='flex size-6 shrink-0 items-center justify-center'
|
||||
onClick={handleClosePreviewPanel}
|
||||
>
|
||||
<RiCloseLine className='size-4 text-text-tertiary' />
|
||||
</button>
|
||||
</div>
|
||||
{/* Data source form Preview */}
|
||||
<DataSource
|
||||
onSelect={setDatasource}
|
||||
dataSourceNodeId={datasource?.nodeId || ''}
|
||||
/>
|
||||
<div className='px-4 py-2'>
|
||||
<Divider type='horizontal' className='bg-divider-subtle' />
|
||||
</div>
|
||||
{/* Process documents form Preview */}
|
||||
<ProcessDocuments dataSourceNodeId={datasource?.nodeId || ''} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PreviewPanel
|
||||
@@ -0,0 +1,31 @@
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import { useDraftPipelineProcessingParams } from '@/service/use-pipeline'
|
||||
import Form from './form'
|
||||
|
||||
type ProcessDocumentsProps = {
|
||||
dataSourceNodeId: string
|
||||
}
|
||||
|
||||
const ProcessDocuments = ({
|
||||
dataSourceNodeId,
|
||||
}: ProcessDocumentsProps) => {
|
||||
const { t } = useTranslation()
|
||||
const pipelineId = useStore(state => state.pipelineId)
|
||||
const { data: paramsConfig } = useDraftPipelineProcessingParams({
|
||||
pipeline_id: pipelineId!,
|
||||
node_id: dataSourceNodeId,
|
||||
}, !!pipelineId && !!dataSourceNodeId)
|
||||
|
||||
return (
|
||||
<div className='flex flex-col'>
|
||||
<div className='system-sm-semibold-uppercase px-4 pt-2 text-text-secondary'>
|
||||
{t('datasetPipeline.inputFieldPanel.preview.stepTwoTitle')}
|
||||
</div>
|
||||
<Form variables={paramsConfig?.variables || []} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(ProcessDocuments)
|
||||
@@ -0,0 +1,39 @@
|
||||
import React, { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import { useWorkflowInteractions } from '@/app/components/workflow/hooks'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
|
||||
const Header = () => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
const { t } = useTranslation()
|
||||
const { handleCancelDebugAndPreviewPanel } = useWorkflowInteractions()
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
const {
|
||||
isPreparingDataSource,
|
||||
setIsPreparingDataSource,
|
||||
} = workflowStore.getState()
|
||||
if (isPreparingDataSource)
|
||||
setIsPreparingDataSource?.(false)
|
||||
handleCancelDebugAndPreviewPanel()
|
||||
}, [workflowStore])
|
||||
|
||||
return (
|
||||
<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>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Header)
|
||||
@@ -0,0 +1,26 @@
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import DataSourceProvider from '@/app/components/datasets/documents/create-from-pipeline/data-source/store/provider'
|
||||
import Preparation from './preparation'
|
||||
import Result from './result'
|
||||
import Header from './header'
|
||||
|
||||
const TestRunPanel = () => {
|
||||
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'
|
||||
>
|
||||
<Header />
|
||||
{isPreparingDataSource ? (
|
||||
<DataSourceProvider>
|
||||
<Preparation />
|
||||
</DataSourceProvider>
|
||||
) : (
|
||||
<Result />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TestRunPanel
|
||||
@@ -0,0 +1,25 @@
|
||||
import React from 'react'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type ActionsProps = {
|
||||
disabled?: boolean
|
||||
handleNextStep: () => void
|
||||
}
|
||||
|
||||
const Actions = ({
|
||||
disabled,
|
||||
handleNextStep,
|
||||
}: ActionsProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className='flex justify-end p-4 pt-2'>
|
||||
<Button disabled={disabled} variant='primary' onClick={handleNextStep}>
|
||||
<span className='px-0.5'>{t('datasetCreation.stepOne.button')}</span>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Actions)
|
||||
@@ -0,0 +1,49 @@
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import { useDatasourceOptions } from '../hooks'
|
||||
import OptionCard from './option-card'
|
||||
import type { Datasource } from '../../types'
|
||||
|
||||
type DataSourceOptionsProps = {
|
||||
dataSourceNodeId: string
|
||||
onSelect: (option: Datasource) => void
|
||||
}
|
||||
|
||||
const DataSourceOptions = ({
|
||||
dataSourceNodeId,
|
||||
onSelect,
|
||||
}: DataSourceOptionsProps) => {
|
||||
const options = useDatasourceOptions()
|
||||
|
||||
const handelSelect = useCallback((value: string) => {
|
||||
const selectedOption = options.find(option => option.value === value)
|
||||
if (!selectedOption)
|
||||
return
|
||||
const datasource = {
|
||||
nodeId: selectedOption.value,
|
||||
nodeData: selectedOption.data,
|
||||
}
|
||||
onSelect(datasource)
|
||||
}, [onSelect, options])
|
||||
|
||||
useEffect(() => {
|
||||
if (options.length > 0 && !dataSourceNodeId)
|
||||
handelSelect(options[0].value)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className='grid w-full grid-cols-4 gap-1'>
|
||||
{options.map(option => (
|
||||
<OptionCard
|
||||
key={option.value}
|
||||
label={option.label}
|
||||
value={option.value}
|
||||
nodeData={option.data}
|
||||
selected={dataSourceNodeId === option.value}
|
||||
onClick={handelSelect}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DataSourceOptions
|
||||
@@ -0,0 +1,55 @@
|
||||
import React, { useCallback } from 'react'
|
||||
import cn from '@/utils/classnames'
|
||||
import BlockIcon from '@/app/components/workflow/block-icon'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import type { DataSourceNodeType } from '@/app/components/workflow/nodes/data-source/types'
|
||||
import { useToolIcon } from '@/app/components/workflow/hooks'
|
||||
|
||||
type OptionCardProps = {
|
||||
label: string
|
||||
value: string
|
||||
selected: boolean
|
||||
nodeData: DataSourceNodeType
|
||||
onClick?: (value: string) => void
|
||||
}
|
||||
|
||||
const OptionCard = ({
|
||||
label,
|
||||
value,
|
||||
selected,
|
||||
nodeData,
|
||||
onClick,
|
||||
}: OptionCardProps) => {
|
||||
const toolIcon = useToolIcon(nodeData)
|
||||
|
||||
const handleClickCard = useCallback(() => {
|
||||
onClick?.(value)
|
||||
}, [value, onClick])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex cursor-pointer flex-col gap-1 rounded-xl border border-components-option-card-option-border bg-components-option-card-option-bg p-2.5 shadow-shadow-shadow-3',
|
||||
selected
|
||||
? 'border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg shadow-xs ring-[0.5px] ring-inset ring-components-option-card-option-selected-border'
|
||||
: 'hover:bg-components-option-card-bg-hover hover:border-components-option-card-option-border-hover hover:shadow-xs',
|
||||
)}
|
||||
onClick={handleClickCard}
|
||||
>
|
||||
<div className='flex size-7 shrink-0 items-center justify-center rounded-lg border-[0.5px] border-components-panel-border bg-background-default-dodge p-1'>
|
||||
<BlockIcon
|
||||
type={BlockEnum.DataSource}
|
||||
toolIcon={toolIcon}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={cn('system-sm-medium line-clamp-2 grow text-text-secondary', selected && 'text-text-primary')}
|
||||
title={label}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(OptionCard)
|
||||
@@ -0,0 +1,46 @@
|
||||
import React from 'react'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { CustomActionsProps } from '@/app/components/base/form/components/form/actions'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import { WorkflowRunningStatus } from '@/app/components/workflow/types'
|
||||
|
||||
type ActionsProps = {
|
||||
formParams: CustomActionsProps
|
||||
runDisabled?: boolean
|
||||
onBack: () => void
|
||||
}
|
||||
|
||||
const Actions = ({
|
||||
formParams,
|
||||
runDisabled,
|
||||
onBack,
|
||||
}: ActionsProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { form, isSubmitting, canSubmit } = formParams
|
||||
const workflowRunningData = useStore(s => s.workflowRunningData)
|
||||
const isRunning = workflowRunningData?.result.status === WorkflowRunningStatus.Running
|
||||
|
||||
return (
|
||||
<div className='flex items-center justify-end gap-x-2 p-4 pt-2'>
|
||||
<Button
|
||||
variant='secondary'
|
||||
onClick={onBack}
|
||||
>
|
||||
{t('datasetPipeline.operations.backToDataSource')}
|
||||
</Button>
|
||||
<Button
|
||||
variant='primary'
|
||||
onClick={() => {
|
||||
form.handleSubmit()
|
||||
}}
|
||||
disabled={runDisabled || isSubmitting || !canSubmit || isRunning}
|
||||
loading={isSubmitting || isRunning}
|
||||
>
|
||||
{t('datasetPipeline.operations.process')}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Actions)
|
||||
@@ -0,0 +1,15 @@
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import { useDraftPipelineProcessingParams } from '@/service/use-pipeline'
|
||||
|
||||
export const useInputVariables = (datasourceNodeId: string) => {
|
||||
const pipelineId = useStore(state => state.pipelineId)
|
||||
const { data: paramsConfig, isFetching: isFetchingParams } = useDraftPipelineProcessingParams({
|
||||
pipeline_id: pipelineId!,
|
||||
node_id: datasourceNodeId,
|
||||
})
|
||||
|
||||
return {
|
||||
isFetchingParams,
|
||||
paramsConfig,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import React, { useCallback } from 'react'
|
||||
import { generateZodSchema } from '@/app/components/base/form/form-scenarios/base/utils'
|
||||
import { useInputVariables } from './hooks'
|
||||
import Options from './options'
|
||||
import Actions from './actions'
|
||||
import type { CustomActionsProps } from '@/app/components/base/form/components/form/actions'
|
||||
import { useConfigurations, useInitialData } from '@/app/components/rag-pipeline/hooks/use-input-fields'
|
||||
|
||||
type DocumentProcessingProps = {
|
||||
dataSourceNodeId: string
|
||||
onProcess: (data: Record<string, any>) => void
|
||||
onBack: () => void
|
||||
}
|
||||
|
||||
const DocumentProcessing = ({
|
||||
dataSourceNodeId,
|
||||
onProcess,
|
||||
onBack,
|
||||
}: DocumentProcessingProps) => {
|
||||
const { isFetchingParams, paramsConfig } = useInputVariables(dataSourceNodeId)
|
||||
const initialData = useInitialData(paramsConfig?.variables || [])
|
||||
const configurations = useConfigurations(paramsConfig?.variables || [])
|
||||
const schema = generateZodSchema(configurations)
|
||||
|
||||
const renderCustomActions = useCallback((props: CustomActionsProps) => (
|
||||
<Actions runDisabled={isFetchingParams} formParams={props} onBack={onBack} />
|
||||
), [isFetchingParams, onBack])
|
||||
|
||||
return (
|
||||
<Options
|
||||
initialData={initialData}
|
||||
configurations={configurations}
|
||||
schema={schema}
|
||||
onSubmit={onProcess}
|
||||
CustomActions={renderCustomActions}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(DocumentProcessing)
|
||||
@@ -0,0 +1,73 @@
|
||||
import { useAppForm } from '@/app/components/base/form'
|
||||
import type { CustomActionsProps } from '@/app/components/base/form/components/form/actions'
|
||||
import BaseField from '@/app/components/base/form/form-scenarios/base/field'
|
||||
import type { BaseConfiguration } from '@/app/components/base/form/form-scenarios/base/types'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import type { ZodSchema } from 'zod'
|
||||
|
||||
type OptionsProps = {
|
||||
initialData: Record<string, any>
|
||||
configurations: BaseConfiguration[]
|
||||
schema: ZodSchema
|
||||
CustomActions: (props: CustomActionsProps) => React.JSX.Element
|
||||
onSubmit: (data: Record<string, any>) => void
|
||||
}
|
||||
|
||||
const Options = ({
|
||||
initialData,
|
||||
configurations,
|
||||
schema,
|
||||
CustomActions,
|
||||
onSubmit,
|
||||
}: OptionsProps) => {
|
||||
const form = useAppForm({
|
||||
defaultValues: initialData,
|
||||
validators: {
|
||||
onSubmit: ({ value }) => {
|
||||
const result = schema.safeParse(value)
|
||||
if (!result.success) {
|
||||
const issues = result.error.issues
|
||||
const firstIssue = issues[0]
|
||||
const errorMessage = `Path: ${firstIssue.path.join('.')} Error: ${firstIssue.message}`
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: errorMessage,
|
||||
})
|
||||
return errorMessage
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
},
|
||||
onSubmit: ({ value }) => {
|
||||
onSubmit(value)
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<form
|
||||
className='w-full'
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
form.handleSubmit()
|
||||
}}
|
||||
>
|
||||
<div className='flex flex-col gap-3 px-4 pb-6 pt-3'>
|
||||
{configurations.map((config, index) => {
|
||||
const FieldComponent = BaseField({
|
||||
initialData,
|
||||
config,
|
||||
})
|
||||
return <FieldComponent key={index} form={form} />
|
||||
})}
|
||||
</div>
|
||||
<form.AppForm>
|
||||
<form.Actions
|
||||
CustomActions={CustomActions}
|
||||
/>
|
||||
</form.AppForm>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
export default Options
|
||||
@@ -0,0 +1,14 @@
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const FooterTips = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className='system-xs-regular flex grow flex-col justify-end p-4 pt-2 text-text-tertiary'>
|
||||
{t('datasetPipeline.testRun.tooltip')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(FooterTips)
|
||||
@@ -0,0 +1,129 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
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'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useDataSourceStore } from '@/app/components/datasets/documents/create-from-pipeline/data-source/store'
|
||||
import { CrawlStep } from '@/models/datasets'
|
||||
|
||||
export const useTestRunSteps = () => {
|
||||
const { t } = useTranslation()
|
||||
const [currentStep, setCurrentStep] = useState(1)
|
||||
|
||||
const handleNextStep = useCallback(() => {
|
||||
setCurrentStep(preStep => preStep + 1)
|
||||
}, [])
|
||||
|
||||
const handleBackStep = useCallback(() => {
|
||||
setCurrentStep(preStep => preStep - 1)
|
||||
}, [])
|
||||
|
||||
const steps = [
|
||||
{
|
||||
label: t('datasetPipeline.testRun.steps.dataSource'),
|
||||
value: TestRunStep.dataSource,
|
||||
},
|
||||
{
|
||||
label: t('datasetPipeline.testRun.steps.documentProcessing'),
|
||||
value: TestRunStep.documentProcessing,
|
||||
},
|
||||
]
|
||||
|
||||
return {
|
||||
steps,
|
||||
currentStep,
|
||||
handleNextStep,
|
||||
handleBackStep,
|
||||
}
|
||||
}
|
||||
|
||||
export const useDatasourceOptions = () => {
|
||||
const nodes = useNodes<DataSourceNodeType>()
|
||||
const datasourceNodes = nodes.filter(node => node.data.type === BlockEnum.DataSource)
|
||||
|
||||
const options = useMemo(() => {
|
||||
const options: DataSourceOption[] = []
|
||||
datasourceNodes.forEach((node) => {
|
||||
const label = node.data.title
|
||||
options.push({
|
||||
label,
|
||||
value: node.id,
|
||||
data: node.data,
|
||||
})
|
||||
})
|
||||
return options
|
||||
}, [datasourceNodes])
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
export const useOnlineDocument = () => {
|
||||
const dataSourceStore = useDataSourceStore()
|
||||
|
||||
const clearOnlineDocumentData = useCallback(() => {
|
||||
const {
|
||||
setDocumentsData,
|
||||
setSearchValue,
|
||||
setSelectedPagesId,
|
||||
setOnlineDocuments,
|
||||
setCurrentDocument,
|
||||
} = dataSourceStore.getState()
|
||||
setDocumentsData([])
|
||||
setSearchValue('')
|
||||
setSelectedPagesId(new Set())
|
||||
setOnlineDocuments([])
|
||||
setCurrentDocument(undefined)
|
||||
}, [dataSourceStore])
|
||||
|
||||
return {
|
||||
clearOnlineDocumentData,
|
||||
}
|
||||
}
|
||||
|
||||
export const useWebsiteCrawl = () => {
|
||||
const dataSourceStore = useDataSourceStore()
|
||||
|
||||
const clearWebsiteCrawlData = useCallback(() => {
|
||||
const {
|
||||
setStep,
|
||||
setCrawlResult,
|
||||
setWebsitePages,
|
||||
setPreviewIndex,
|
||||
setCurrentWebsite,
|
||||
} = dataSourceStore.getState()
|
||||
setStep(CrawlStep.init)
|
||||
setCrawlResult(undefined)
|
||||
setCurrentWebsite(undefined)
|
||||
setWebsitePages([])
|
||||
setPreviewIndex(-1)
|
||||
}, [dataSourceStore])
|
||||
|
||||
return {
|
||||
clearWebsiteCrawlData,
|
||||
}
|
||||
}
|
||||
|
||||
export const useOnlineDrive = () => {
|
||||
const dataSourceStore = useDataSourceStore()
|
||||
|
||||
const clearOnlineDriveData = useCallback(() => {
|
||||
const {
|
||||
setOnlineDriveFileList,
|
||||
setBucket,
|
||||
setPrefix,
|
||||
setKeywords,
|
||||
setSelectedFileIds,
|
||||
} = dataSourceStore.getState()
|
||||
setOnlineDriveFileList([])
|
||||
setBucket('')
|
||||
setPrefix([])
|
||||
setKeywords('')
|
||||
setSelectedFileIds([])
|
||||
}, [dataSourceStore])
|
||||
|
||||
return {
|
||||
clearOnlineDriveData,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
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,
|
||||
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 !localFileList.length || localFileList.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, localFileList, 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, onlineDriveFileList, selectedFileIds } = dataSourceStore.getState()
|
||||
const file = onlineDriveFileList.find(file => file.id === selectedFileIds[0])
|
||||
datasourceInfoList.push({
|
||||
bucket,
|
||||
id: file?.id,
|
||||
name: file?.name,
|
||||
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 // 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)
|
||||
@@ -0,0 +1,44 @@
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import cn from '@/utils/classnames'
|
||||
import React from 'react'
|
||||
|
||||
type Step = {
|
||||
label: string
|
||||
value: string
|
||||
}
|
||||
|
||||
type StepIndicatorProps = {
|
||||
currentStep: number
|
||||
steps: Step[]
|
||||
}
|
||||
|
||||
const StepIndicator = ({
|
||||
currentStep,
|
||||
steps,
|
||||
}: StepIndicatorProps) => {
|
||||
return (
|
||||
<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
|
||||
return (
|
||||
<div key={index} className='flex items-center gap-x-2'>
|
||||
<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' />}
|
||||
<span className='system-2xs-semibold-uppercase'>{step.label}</span>
|
||||
</div>
|
||||
{!isLastStep && (
|
||||
<div className='flex items-center'>
|
||||
<Divider type='horizontal' className='h-px w-3 bg-divider-deep' />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(StepIndicator)
|
||||
@@ -0,0 +1,70 @@
|
||||
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}
|
||||
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 grow 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 grow items-center justify-center'>
|
||||
<Loading />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(Result)
|
||||
@@ -0,0 +1,60 @@
|
||||
import Button from '@/app/components/base/button'
|
||||
import { RiLoader2Line } from '@remixicon/react'
|
||||
import React, { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ChunkCardList } from '../../../../chunk-card-list'
|
||||
import { RAG_PIPELINE_PREVIEW_CHUNK_NUM } from '@/config'
|
||||
import { formatPreviewChunks } from './utils'
|
||||
|
||||
type ResultTextProps = {
|
||||
isRunning?: boolean
|
||||
outputs?: any
|
||||
error?: string
|
||||
onSwitchToDetail: () => void
|
||||
}
|
||||
|
||||
const ResultPreview = ({
|
||||
isRunning,
|
||||
outputs,
|
||||
error,
|
||||
onSwitchToDetail,
|
||||
}: ResultTextProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const previewChunks = useMemo(() => {
|
||||
return formatPreviewChunks(outputs)
|
||||
}, [outputs])
|
||||
|
||||
return (
|
||||
<>
|
||||
{isRunning && !outputs && (
|
||||
<div className='flex grow flex-col items-center justify-center gap-y-2 pb-20'>
|
||||
<RiLoader2Line className='size-4 animate-spin text-text-tertiary' />
|
||||
<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 && previewChunks && (
|
||||
<div className='flex grow flex-col bg-background-body p-1'>
|
||||
<ChunkCardList chunkType={outputs.chunk_structure} chunkInfo={previewChunks} />
|
||||
<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: RAG_PIPELINE_PREVIEW_CHUNK_NUM })}>
|
||||
{t('pipeline.result.resultPreview.footerTip', { count: RAG_PIPELINE_PREVIEW_CHUNK_NUM })}
|
||||
</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,88 @@
|
||||
import { RAG_PIPELINE_PREVIEW_CHUNK_NUM } from '@/config'
|
||||
import type { ChunkInfo, GeneralChunks, ParentChildChunks, QAChunks } from '../../../../chunk-card-list/types'
|
||||
import type { ParentMode } from '@/models/datasets'
|
||||
import { ChunkingMode } from '@/models/datasets'
|
||||
|
||||
type GeneralChunkPreview = {
|
||||
content: string
|
||||
}
|
||||
|
||||
const formatGeneralChunks = (outputs: any) => {
|
||||
const chunkInfo: GeneralChunks = []
|
||||
const chunks = outputs.preview as GeneralChunkPreview[]
|
||||
chunks.slice(0, RAG_PIPELINE_PREVIEW_CHUNK_NUM).forEach((chunk) => {
|
||||
chunkInfo.push(chunk.content)
|
||||
})
|
||||
|
||||
return chunkInfo
|
||||
}
|
||||
|
||||
type ParentChildChunkPreview = {
|
||||
content: string
|
||||
child_chunks: string[]
|
||||
}
|
||||
|
||||
const formatParentChildChunks = (outputs: any, parentMode: ParentMode) => {
|
||||
const chunkInfo: ParentChildChunks = {
|
||||
parent_child_chunks: [],
|
||||
parent_mode: parentMode,
|
||||
}
|
||||
const chunks = outputs.preview as ParentChildChunkPreview[]
|
||||
if (parentMode === 'paragraph') {
|
||||
chunks.slice(0, RAG_PIPELINE_PREVIEW_CHUNK_NUM).forEach((chunk) => {
|
||||
chunkInfo.parent_child_chunks?.push({
|
||||
parent_content: chunk.content,
|
||||
child_contents: chunk.child_chunks,
|
||||
parent_mode: parentMode,
|
||||
})
|
||||
})
|
||||
}
|
||||
if (parentMode === 'full-doc') {
|
||||
chunks.forEach((chunk) => {
|
||||
chunkInfo.parent_child_chunks?.push({
|
||||
parent_content: chunk.content,
|
||||
child_contents: chunk.child_chunks.slice(0, RAG_PIPELINE_PREVIEW_CHUNK_NUM),
|
||||
parent_mode: parentMode,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return chunkInfo
|
||||
}
|
||||
|
||||
type QAChunkPreview = {
|
||||
question: string
|
||||
answer: string
|
||||
}
|
||||
|
||||
const formatQAChunks = (outputs: any) => {
|
||||
const chunkInfo: QAChunks = {
|
||||
qa_chunks: [],
|
||||
}
|
||||
const chunks = outputs.qa_preview as QAChunkPreview[]
|
||||
chunks.slice(0, RAG_PIPELINE_PREVIEW_CHUNK_NUM).forEach((chunk) => {
|
||||
chunkInfo.qa_chunks?.push({
|
||||
...chunk,
|
||||
})
|
||||
})
|
||||
|
||||
return chunkInfo
|
||||
}
|
||||
|
||||
export const formatPreviewChunks = (outputs: any): ChunkInfo | undefined => {
|
||||
if (!outputs) return undefined
|
||||
|
||||
const chunkingMode = outputs.chunk_structure
|
||||
const parentMode = outputs.parent_mode
|
||||
|
||||
if (chunkingMode === ChunkingMode.text)
|
||||
return formatGeneralChunks(outputs)
|
||||
|
||||
if (chunkingMode === ChunkingMode.parentChild)
|
||||
return formatParentChildChunks(outputs, parentMode)
|
||||
|
||||
if (chunkingMode === ChunkingMode.qa)
|
||||
return formatQAChunks(outputs)
|
||||
|
||||
return undefined
|
||||
}
|
||||
@@ -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)
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { DataSourceNodeType } from '@/app/components/workflow/nodes/data-source/types'
|
||||
|
||||
export enum TestRunStep {
|
||||
dataSource = 'dataSource',
|
||||
documentProcessing = 'documentProcessing',
|
||||
}
|
||||
|
||||
export type DataSourceOption = {
|
||||
label: string
|
||||
value: string
|
||||
data: DataSourceNodeType
|
||||
}
|
||||
|
||||
export type Datasource = {
|
||||
nodeId: string
|
||||
nodeData: DataSourceNodeType
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
'use client'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import AppIconPicker from '@/app/components/base/app-icon-picker'
|
||||
import type { AppIconSelection } from '@/app/components/base/app-icon-picker'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import { noop } from 'lodash-es'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import type { IconInfo } from '@/models/datasets'
|
||||
|
||||
type PublishAsKnowledgePipelineModalProps = {
|
||||
confirmDisabled?: boolean
|
||||
onCancel: () => void
|
||||
onConfirm: (
|
||||
name: string,
|
||||
icon: IconInfo,
|
||||
description?: string,
|
||||
) => Promise<void>
|
||||
}
|
||||
const PublishAsKnowledgePipelineModal = ({
|
||||
confirmDisabled,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
}: PublishAsKnowledgePipelineModalProps) => {
|
||||
const { t } = useTranslation()
|
||||
const knowledgeName = useStore(s => s.knowledgeName)
|
||||
const knowledgeIcon = useStore(s => s.knowledgeIcon)
|
||||
const [pipelineName, setPipelineName] = useState(knowledgeName!)
|
||||
const [pipelineIcon, setPipelineIcon] = useState(knowledgeIcon!)
|
||||
const [description, setDescription] = useState('')
|
||||
const [showAppIconPicker, setShowAppIconPicker] = useState(false)
|
||||
|
||||
const handleSelectIcon = useCallback((item: AppIconSelection) => {
|
||||
if (item.type === 'image') {
|
||||
setPipelineIcon({
|
||||
icon_type: 'image',
|
||||
icon_url: item.url,
|
||||
icon_background: '',
|
||||
icon: '',
|
||||
})
|
||||
}
|
||||
|
||||
if (item.type === 'emoji') {
|
||||
setPipelineIcon({
|
||||
icon_type: 'emoji',
|
||||
icon: item.icon,
|
||||
icon_background: item.background,
|
||||
icon_url: '',
|
||||
})
|
||||
}
|
||||
setShowAppIconPicker(false)
|
||||
}, [])
|
||||
const handleCloseIconPicker = useCallback(() => {
|
||||
setPipelineIcon({
|
||||
icon_type: pipelineIcon.icon_type,
|
||||
icon: pipelineIcon.icon,
|
||||
icon_background: pipelineIcon.icon_background,
|
||||
icon_url: pipelineIcon.icon_url,
|
||||
})
|
||||
setShowAppIconPicker(false)
|
||||
}, [pipelineIcon])
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (confirmDisabled)
|
||||
return
|
||||
|
||||
onConfirm(
|
||||
pipelineName?.trim() || '',
|
||||
pipelineIcon,
|
||||
description?.trim(),
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
isShow
|
||||
onClose={noop}
|
||||
className='relative !w-[520px] !p-0'
|
||||
>
|
||||
<div className='title-2xl-semi-bold relative flex items-center p-6 pb-3 pr-14 text-text-primary'>
|
||||
{t('pipeline.common.publishAs')}
|
||||
<div className='absolute right-5 top-5 flex h-8 w-8 cursor-pointer items-center justify-center' onClick={onCancel}>
|
||||
<RiCloseLine className='h-4 w-4 text-text-tertiary' />
|
||||
</div>
|
||||
</div>
|
||||
<div className='px-6 py-3'>
|
||||
<div className='mb-5 flex'>
|
||||
<div className='mr-3 grow'>
|
||||
<div className='system-sm-medium mb-1 flex h-6 items-center text-text-secondary'>
|
||||
{t('pipeline.common.publishAsPipeline.name')}
|
||||
</div>
|
||||
<Input
|
||||
value={pipelineName}
|
||||
onChange={e => setPipelineName(e.target.value)}
|
||||
placeholder={t('pipeline.common.publishAsPipeline.namePlaceholder') || ''}
|
||||
/>
|
||||
</div>
|
||||
<AppIcon
|
||||
size='xxl'
|
||||
onClick={() => { setShowAppIconPicker(true) }}
|
||||
className='mt-2 shrink-0 cursor-pointer'
|
||||
iconType={pipelineIcon?.icon_type}
|
||||
icon={pipelineIcon?.icon}
|
||||
background={pipelineIcon?.icon_background}
|
||||
imageUrl={pipelineIcon?.icon_url}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className='system-sm-medium mb-1 flex h-6 items-center text-text-secondary '>
|
||||
{t('pipeline.common.publishAsPipeline.description')}
|
||||
</div>
|
||||
<Textarea
|
||||
className='resize-none'
|
||||
placeholder={t('pipeline.common.publishAsPipeline.descriptionPlaceholder') || ''}
|
||||
value={description}
|
||||
onChange={e => setDescription(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-center justify-end px-6 py-5'>
|
||||
<Button
|
||||
className='mr-2'
|
||||
onClick={onCancel}
|
||||
>
|
||||
{t('common.operation.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={!pipelineName?.trim() || confirmDisabled}
|
||||
variant='primary'
|
||||
onClick={() => handleConfirm()}
|
||||
>
|
||||
{t('workflow.common.publish')}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
{showAppIconPicker && <AppIconPicker
|
||||
onSelect={handleSelectIcon}
|
||||
onClose={handleCloseIconPicker}
|
||||
/>}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default PublishAsKnowledgePipelineModal
|
||||
@@ -0,0 +1,47 @@
|
||||
import {
|
||||
memo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiCloseLine,
|
||||
RiInformation2Fill,
|
||||
} from '@remixicon/react'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
|
||||
const PublishToast = () => {
|
||||
const { t } = useTranslation()
|
||||
const publishedAt = useStore(s => s.publishedAt)
|
||||
const [hideToast, setHideToast] = useState(false)
|
||||
|
||||
if (publishedAt || hideToast)
|
||||
return null
|
||||
|
||||
return (
|
||||
<div className='pointer-events-none absolute bottom-[45px] left-0 right-0 z-10 flex justify-center'>
|
||||
<div
|
||||
className='relative flex w-[420px] space-x-1 overflow-hidden rounded-xl border border-components-panel-border bg-components-panel-bg-blur p-3 shadow-lg'
|
||||
>
|
||||
<div className='pointer-events-none absolute inset-0 bg-gradient-to-r from-components-badge-status-light-normal-halo to-background-gradient-mask-transparent opacity-[0.4]'>
|
||||
</div>
|
||||
<div className='flex h-6 w-6 items-center justify-center'>
|
||||
<RiInformation2Fill className='text-text-accent' />
|
||||
</div>
|
||||
<div className='p-1'>
|
||||
<div className='system-sm-semibold mb-1 text-text-primary'>{t('pipeline.publishToast.title')}</div>
|
||||
<div className='system-xs-regular text-text-secondary'>
|
||||
{t('pipeline.publishToast.desc')}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className='pointer-events-auto flex h-6 w-6 cursor-pointer items-center justify-center'
|
||||
onClick={() => setHideToast(true)}
|
||||
>
|
||||
<RiCloseLine className='h-4 w-4 text-text-tertiary' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(PublishToast)
|
||||
@@ -0,0 +1,70 @@
|
||||
import {
|
||||
memo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useStore } from '../../workflow/store'
|
||||
import PluginDependency from '../../workflow/plugin-dependency'
|
||||
import RagPipelinePanel from './panel'
|
||||
import RagPipelineHeader from './rag-pipeline-header'
|
||||
import type { EnvironmentVariable } from '@/app/components/workflow/types'
|
||||
import { DSL_EXPORT_CHECK } from '@/app/components/workflow/constants'
|
||||
import UpdateDSLModal from './update-dsl-modal'
|
||||
import DSLExportConfirmModal from '@/app/components/workflow/dsl-export-confirm-modal'
|
||||
import {
|
||||
useDSL,
|
||||
usePanelInteractions,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import PublishToast from './publish-toast'
|
||||
import { useRagPipelineSearch } from '../hooks/use-rag-pipeline-search'
|
||||
|
||||
const RagPipelineChildren = () => {
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
const [secretEnvList, setSecretEnvList] = useState<EnvironmentVariable[]>([])
|
||||
const showImportDSLModal = useStore(s => s.showImportDSLModal)
|
||||
const setShowImportDSLModal = useStore(s => s.setShowImportDSLModal)
|
||||
const {
|
||||
handlePaneContextmenuCancel,
|
||||
} = usePanelInteractions()
|
||||
const {
|
||||
exportCheck,
|
||||
handleExportDSL,
|
||||
} = useDSL()
|
||||
|
||||
// Initialize RAG pipeline search functionality
|
||||
useRagPipelineSearch()
|
||||
|
||||
eventEmitter?.useSubscription((v: any) => {
|
||||
if (v.type === DSL_EXPORT_CHECK)
|
||||
setSecretEnvList(v.payload.data as EnvironmentVariable[])
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<PluginDependency />
|
||||
{
|
||||
showImportDSLModal && (
|
||||
<UpdateDSLModal
|
||||
onCancel={() => setShowImportDSLModal(false)}
|
||||
onBackup={exportCheck!}
|
||||
onImport={handlePaneContextmenuCancel}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
secretEnvList.length > 0 && (
|
||||
<DSLExportConfirmModal
|
||||
envList={secretEnvList}
|
||||
onConfirm={handleExportDSL!}
|
||||
onClose={() => setSecretEnvList([])}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<RagPipelineHeader />
|
||||
<RagPipelinePanel />
|
||||
<PublishToast />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(RagPipelineChildren)
|
||||
@@ -0,0 +1,54 @@
|
||||
import {
|
||||
memo,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { HeaderProps } from '@/app/components/workflow/header'
|
||||
import Header from '@/app/components/workflow/header'
|
||||
import { fetchWorkflowRunHistory } from '@/service/workflow'
|
||||
import {
|
||||
useStore,
|
||||
} 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 pipelineId = useStore(s => s.pipelineId)
|
||||
const showDebugAndPreviewPanel = useStore(s => s.showDebugAndPreviewPanel)
|
||||
|
||||
const viewHistoryProps = useMemo(() => {
|
||||
return {
|
||||
historyUrl: `/rag/pipelines/${pipelineId}/workflow-runs`,
|
||||
historyFetcher: fetchWorkflowRunHistory,
|
||||
}
|
||||
}, [pipelineId])
|
||||
|
||||
const headerProps: HeaderProps = useMemo(() => {
|
||||
return {
|
||||
normal: {
|
||||
components: {
|
||||
left: <InputFieldButton />,
|
||||
middle: <Publisher />,
|
||||
},
|
||||
runAndHistoryProps: {
|
||||
showRunButton: true,
|
||||
viewHistoryProps,
|
||||
components: {
|
||||
RunMode,
|
||||
},
|
||||
},
|
||||
},
|
||||
viewHistory: {
|
||||
viewHistoryProps,
|
||||
},
|
||||
}
|
||||
}, [viewHistoryProps, showDebugAndPreviewPanel, t])
|
||||
|
||||
return (
|
||||
<Header {...headerProps} />
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(RagPipelineHeader)
|
||||
@@ -0,0 +1,28 @@
|
||||
import Button from '@/app/components/base/button'
|
||||
import { InputField } from '@/app/components/base/icons/src/vender/pipeline'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const InputFieldButton = () => {
|
||||
const { t } = useTranslation()
|
||||
const setShowInputFieldPanel = useStore(state => state.setShowInputFieldPanel)
|
||||
const setShowEnvPanel = useStore(state => state.setShowEnvPanel)
|
||||
const handleClick = useCallback(() => {
|
||||
setShowInputFieldPanel?.(true)
|
||||
setShowEnvPanel(false)
|
||||
}, [setShowInputFieldPanel, setShowEnvPanel])
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant='secondary'
|
||||
className='flex gap-x-0.5'
|
||||
onClick={handleClick}
|
||||
>
|
||||
<InputField className='h-4 w-4' />
|
||||
<span className='px-0.5'>{t('datasetPipeline.inputField')}</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export default InputFieldButton
|
||||
@@ -0,0 +1,54 @@
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiArrowDownSLine } from '@remixicon/react'
|
||||
import Button from '@/app/components/base/button'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import { useNodesSyncDraft } from '@/app/components/workflow/hooks'
|
||||
import Popup from './popup'
|
||||
|
||||
const Publisher = () => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
|
||||
const handleOpenChange = useCallback((newOpen: boolean) => {
|
||||
if (newOpen)
|
||||
handleSyncWorkflowDraft(true)
|
||||
setOpen(newOpen)
|
||||
}, [handleSyncWorkflowDraft])
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement='bottom-end'
|
||||
offset={{
|
||||
mainAxis: 4,
|
||||
crossAxis: 40,
|
||||
}}
|
||||
>
|
||||
<PortalToFollowElemTrigger onClick={() => handleOpenChange(!open)}>
|
||||
<Button
|
||||
className='px-2'
|
||||
variant='primary'
|
||||
>
|
||||
<span className='pl-1'>{t('workflow.common.publish')}</span>
|
||||
<RiArrowDownSLine className='h-4 w-4' />
|
||||
</Button>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-[11]'>
|
||||
<Popup />
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(Publisher)
|
||||
@@ -0,0 +1,347 @@
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useState,
|
||||
} from 'react'
|
||||
import {
|
||||
RiArrowRightUpLine,
|
||||
RiHammerLine,
|
||||
RiPlayCircleLine,
|
||||
RiTerminalBoxLine,
|
||||
} from '@remixicon/react'
|
||||
import {
|
||||
useBoolean,
|
||||
useKeyPress,
|
||||
} from 'ahooks'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import {
|
||||
useStore,
|
||||
useWorkflowStore,
|
||||
} from '@/app/components/workflow/store'
|
||||
import Button from '@/app/components/base/button'
|
||||
import {
|
||||
useChecklistBeforePublish,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import { getKeyboardKeyCodeBySystem, getKeyboardKeyNameBySystem } from '@/app/components/workflow/utils'
|
||||
import { usePublishWorkflow } from '@/service/use-workflow'
|
||||
import type { PublishWorkflowParams } from '@/types/workflow'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import { useParams, useRouter } from 'next/navigation'
|
||||
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
|
||||
import { useInvalid } from '@/service/use-base'
|
||||
import {
|
||||
publishedPipelineInfoQueryKeyPrefix,
|
||||
useInvalidCustomizedTemplateList,
|
||||
usePublishAsCustomizedPipeline,
|
||||
} from '@/service/use-pipeline'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
import PublishAsKnowledgePipelineModal from '../../publish-as-knowledge-pipeline-modal'
|
||||
import type { IconInfo } from '@/models/datasets'
|
||||
import { useInvalidDatasetList } from '@/service/knowledge/use-dataset'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import classNames from '@/utils/classnames'
|
||||
import PremiumBadge from '@/app/components/base/premium-badge'
|
||||
import { SparklesSoft } from '@/app/components/base/icons/src/public/common'
|
||||
import { useModalContextSelector } from '@/context/modal-context'
|
||||
import Link from 'next/link'
|
||||
import { useDatasetApiAccessUrl } from '@/hooks/use-api-access-url'
|
||||
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
|
||||
|
||||
const PUBLISH_SHORTCUT = ['ctrl', '⇧', 'P']
|
||||
|
||||
const Popup = () => {
|
||||
const { t } = useTranslation()
|
||||
const { datasetId } = useParams()
|
||||
const { push } = useRouter()
|
||||
const publishedAt = useStore(s => s.publishedAt)
|
||||
const draftUpdatedAt = useStore(s => s.draftUpdatedAt)
|
||||
const pipelineId = useStore(s => s.pipelineId)
|
||||
const mutateDatasetRes = useDatasetDetailContextWithSelector(s => s.mutateDatasetRes)
|
||||
const [published, setPublished] = useState(false)
|
||||
const { formatTimeFromNow } = useFormatTimeFromNow()
|
||||
const { handleCheckBeforePublish } = useChecklistBeforePublish()
|
||||
const { mutateAsync: publishWorkflow } = usePublishWorkflow()
|
||||
const { notify } = useToastContext()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const { isAllowPublishAsCustomKnowledgePipelineTemplate } = useProviderContext()
|
||||
const setShowPricingModal = useModalContextSelector(s => s.setShowPricingModal)
|
||||
const apiReferenceUrl = useDatasetApiAccessUrl()
|
||||
|
||||
const [confirmVisible, {
|
||||
setFalse: hideConfirm,
|
||||
setTrue: showConfirm,
|
||||
}] = useBoolean(false)
|
||||
const [publishing, {
|
||||
setFalse: hidePublishing,
|
||||
setTrue: showPublishing,
|
||||
}] = useBoolean(false)
|
||||
const {
|
||||
mutateAsync: publishAsCustomizedPipeline,
|
||||
} = usePublishAsCustomizedPipeline()
|
||||
const [showPublishAsKnowledgePipelineModal, {
|
||||
setFalse: hidePublishAsKnowledgePipelineModal,
|
||||
setTrue: setShowPublishAsKnowledgePipelineModal,
|
||||
}] = useBoolean(false)
|
||||
const [isPublishingAsCustomizedPipeline, {
|
||||
setFalse: hidePublishingAsCustomizedPipeline,
|
||||
setTrue: showPublishingAsCustomizedPipeline,
|
||||
}] = useBoolean(false)
|
||||
|
||||
const invalidPublishedPipelineInfo = useInvalid([...publishedPipelineInfoQueryKeyPrefix, pipelineId])
|
||||
const invalidDatasetList = useInvalidDatasetList()
|
||||
|
||||
const handlePublish = useCallback(async (params?: PublishWorkflowParams) => {
|
||||
if (publishing)
|
||||
return
|
||||
try {
|
||||
const checked = await handleCheckBeforePublish()
|
||||
|
||||
if (checked) {
|
||||
if (!publishedAt && !confirmVisible) {
|
||||
showConfirm()
|
||||
return
|
||||
}
|
||||
showPublishing()
|
||||
const res = await publishWorkflow({
|
||||
url: `/rag/pipelines/${pipelineId}/workflows/publish`,
|
||||
title: params?.title || '',
|
||||
releaseNotes: params?.releaseNotes || '',
|
||||
})
|
||||
setPublished(true)
|
||||
if (res) {
|
||||
notify({
|
||||
type: 'success',
|
||||
message: t('datasetPipeline.publishPipeline.success.message'),
|
||||
children: (
|
||||
<div className='system-xs-regular text-text-secondary'>
|
||||
<Trans
|
||||
i18nKey='datasetPipeline.publishPipeline.success.tip'
|
||||
components={{
|
||||
CustomLink: (
|
||||
<Link
|
||||
className='system-xs-medium text-text-accent'
|
||||
href={`/datasets/${datasetId}/documents`}
|
||||
>
|
||||
</Link>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
})
|
||||
workflowStore.getState().setPublishedAt(res.created_at)
|
||||
mutateDatasetRes?.()
|
||||
invalidPublishedPipelineInfo()
|
||||
invalidDatasetList()
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
notify({ type: 'error', message: t('datasetPipeline.publishPipeline.error.message') })
|
||||
}
|
||||
finally {
|
||||
if (publishing)
|
||||
hidePublishing()
|
||||
if (confirmVisible)
|
||||
hideConfirm()
|
||||
}
|
||||
}, [handleCheckBeforePublish, publishWorkflow, pipelineId, notify, t, workflowStore, mutateDatasetRes, invalidPublishedPipelineInfo, showConfirm, publishedAt, confirmVisible, hidePublishing, showPublishing, hideConfirm, publishing])
|
||||
|
||||
useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.shift.p`, (e) => {
|
||||
e.preventDefault()
|
||||
if (published)
|
||||
return
|
||||
handlePublish()
|
||||
}, { exactMatch: true, useCapture: true })
|
||||
|
||||
const goToAddDocuments = useCallback(() => {
|
||||
push(`/datasets/${datasetId}/documents/create-from-pipeline`)
|
||||
}, [datasetId, push])
|
||||
|
||||
const invalidCustomizedTemplateList = useInvalidCustomizedTemplateList()
|
||||
|
||||
const handlePublishAsKnowledgePipeline = useCallback(async (
|
||||
name: string,
|
||||
icon: IconInfo,
|
||||
description?: string,
|
||||
) => {
|
||||
try {
|
||||
showPublishingAsCustomizedPipeline()
|
||||
await publishAsCustomizedPipeline({
|
||||
pipelineId: pipelineId || '',
|
||||
name,
|
||||
icon_info: icon,
|
||||
description,
|
||||
})
|
||||
notify({
|
||||
type: 'success',
|
||||
message: t('datasetPipeline.publishTemplate.success.message'),
|
||||
children: (
|
||||
<div className='flex flex-col gap-y-1'>
|
||||
<span className='system-xs-regular text-text-secondary'>
|
||||
{t('datasetPipeline.publishTemplate.success.tip')}
|
||||
</span>
|
||||
<Link
|
||||
href='https://docs.dify.ai'
|
||||
target='_blank'
|
||||
className='system-xs-medium-uppercase inline-block text-text-accent'
|
||||
>
|
||||
{t('datasetPipeline.publishTemplate.success.learnMore')}
|
||||
</Link>
|
||||
</div>
|
||||
),
|
||||
})
|
||||
invalidCustomizedTemplateList()
|
||||
}
|
||||
catch {
|
||||
notify({ type: 'error', message: t('datasetPipeline.publishTemplate.error.message') })
|
||||
}
|
||||
finally {
|
||||
hidePublishingAsCustomizedPipeline()
|
||||
hidePublishAsKnowledgePipelineModal()
|
||||
}
|
||||
}, [
|
||||
pipelineId,
|
||||
publishAsCustomizedPipeline,
|
||||
showPublishingAsCustomizedPipeline,
|
||||
hidePublishingAsCustomizedPipeline,
|
||||
hidePublishAsKnowledgePipelineModal,
|
||||
notify,
|
||||
t,
|
||||
])
|
||||
|
||||
const handleClickPublishAsKnowledgePipeline = useCallback(() => {
|
||||
if (!isAllowPublishAsCustomKnowledgePipelineTemplate)
|
||||
setShowPricingModal()
|
||||
else
|
||||
setShowPublishAsKnowledgePipelineModal()
|
||||
}, [isAllowPublishAsCustomKnowledgePipelineTemplate, setShowPublishAsKnowledgePipelineModal, setShowPricingModal])
|
||||
|
||||
return (
|
||||
<div className={classNames(
|
||||
'rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-xl shadow-shadow-shadow-5',
|
||||
isAllowPublishAsCustomKnowledgePipelineTemplate ? 'w-[360px]' : 'w-[400px]',
|
||||
)}>
|
||||
<div className='p-4 pt-3'>
|
||||
<div className='system-xs-medium-uppercase flex h-6 items-center text-text-tertiary'>
|
||||
{publishedAt ? t('workflow.common.latestPublished') : t('workflow.common.currentDraftUnpublished')}
|
||||
</div>
|
||||
{
|
||||
publishedAt
|
||||
? (
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='system-sm-medium flex items-center text-text-secondary'>
|
||||
{t('workflow.common.publishedAt')} {formatTimeFromNow(publishedAt)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div className='system-sm-medium flex items-center text-text-secondary'>
|
||||
{t('workflow.common.autoSaved')} · {Boolean(draftUpdatedAt) && formatTimeFromNow(draftUpdatedAt!)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<Button
|
||||
variant='primary'
|
||||
className='mt-3 w-full'
|
||||
onClick={() => handlePublish()}
|
||||
disabled={published || publishing}
|
||||
>
|
||||
{
|
||||
published
|
||||
? t('workflow.common.published')
|
||||
: (
|
||||
<div className='flex gap-1'>
|
||||
<span>{t('workflow.common.publishUpdate')}</span>
|
||||
<div className='flex gap-0.5'>
|
||||
{PUBLISH_SHORTCUT.map(key => (
|
||||
<span key={key} className='system-kbd h-4 w-4 rounded-[4px] bg-components-kbd-bg-white text-text-primary-on-surface'>
|
||||
{getKeyboardKeyNameBySystem(key)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</Button>
|
||||
</div>
|
||||
<div className='border-t-[0.5px] border-t-divider-regular p-4 pt-3'>
|
||||
<Button
|
||||
className='mb-1 w-full hover:bg-state-accent-hover hover:text-text-accent'
|
||||
variant='tertiary'
|
||||
onClick={goToAddDocuments}
|
||||
disabled={!publishedAt}
|
||||
>
|
||||
<div className='flex grow items-center'>
|
||||
<RiPlayCircleLine className='mr-2 h-4 w-4' />
|
||||
{t('pipeline.common.goToAddDocuments')}
|
||||
</div>
|
||||
<RiArrowRightUpLine className='ml-2 h-4 w-4 shrink-0' />
|
||||
</Button>
|
||||
<Link
|
||||
href={apiReferenceUrl}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
>
|
||||
<Button
|
||||
className='w-full hover:bg-state-accent-hover hover:text-text-accent'
|
||||
variant='tertiary'
|
||||
disabled={!publishedAt}
|
||||
>
|
||||
<div className='flex grow items-center'>
|
||||
<RiTerminalBoxLine className='mr-2 h-4 w-4' />
|
||||
{t('workflow.common.accessAPIReference')}
|
||||
</div>
|
||||
<RiArrowRightUpLine className='ml-2 h-4 w-4 shrink-0' />
|
||||
</Button>
|
||||
</Link>
|
||||
<Divider className='my-2' />
|
||||
<Button
|
||||
className='w-full hover:bg-state-accent-hover hover:text-text-accent'
|
||||
variant='tertiary'
|
||||
onClick={handleClickPublishAsKnowledgePipeline}
|
||||
disabled={!publishedAt || isPublishingAsCustomizedPipeline}
|
||||
>
|
||||
<div className='flex grow items-center gap-x-2 overflow-hidden'>
|
||||
<RiHammerLine className='h-4 w-4 shrink-0' />
|
||||
<span className='grow truncate text-left' title={t('pipeline.common.publishAs')}>
|
||||
{t('pipeline.common.publishAs')}
|
||||
</span>
|
||||
{!isAllowPublishAsCustomKnowledgePipelineTemplate && (
|
||||
<PremiumBadge className='shrink-0 cursor-pointer select-none' size='s' color='indigo'>
|
||||
<SparklesSoft className='flex size-3 items-center text-components-premium-badge-indigo-text-stop-0' />
|
||||
<span className='system-2xs-medium p-0.5'>
|
||||
{t('billing.upgradeBtn.encourageShort')}
|
||||
</span>
|
||||
</PremiumBadge>
|
||||
)}
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
{
|
||||
confirmVisible && (
|
||||
<Confirm
|
||||
isShow={confirmVisible}
|
||||
title={t('pipeline.common.confirmPublish')}
|
||||
content={t('pipeline.common.confirmPublishContent')}
|
||||
onCancel={hideConfirm}
|
||||
onConfirm={handlePublish}
|
||||
isDisabled={publishing}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
showPublishAsKnowledgePipelineModal && (
|
||||
<PublishAsKnowledgePipelineModal
|
||||
confirmDisabled={isPublishingAsCustomizedPipeline}
|
||||
onConfirm={handlePublishAsKnowledgePipeline}
|
||||
onCancel={hidePublishAsKnowledgePipelineModal}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(Popup)
|
||||
@@ -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)
|
||||
@@ -0,0 +1,168 @@
|
||||
import {
|
||||
useCallback,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import { WorkflowWithInnerContext } from '@/app/components/workflow'
|
||||
import type { WorkflowProps } from '@/app/components/workflow'
|
||||
import RagPipelineChildren from './rag-pipeline-children'
|
||||
import {
|
||||
useAvailableNodesMetaData,
|
||||
useDSL,
|
||||
useGetRunAndTraceUrl,
|
||||
useNodesSyncDraft,
|
||||
usePipelineRefreshDraft,
|
||||
usePipelineRun,
|
||||
usePipelineStartRun,
|
||||
} from '../hooks'
|
||||
import { useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import { useConfigsMap } from '../hooks/use-configs-map'
|
||||
import { useSetWorkflowVarsWithValue } from '@/app/components/workflow/hooks/use-fetch-workflow-inspect-vars'
|
||||
import { useInspectVarsCrud } from '../hooks/use-inspect-vars-crud'
|
||||
|
||||
type RagPipelineMainProps = Pick<WorkflowProps, 'nodes' | 'edges' | 'viewport'>
|
||||
const RagPipelineMain = ({
|
||||
nodes,
|
||||
edges,
|
||||
viewport,
|
||||
}: RagPipelineMainProps) => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
const handleWorkflowDataUpdate = useCallback((payload: any) => {
|
||||
const {
|
||||
rag_pipeline_variables,
|
||||
environment_variables,
|
||||
} = payload
|
||||
if (rag_pipeline_variables) {
|
||||
const { setRagPipelineVariables } = workflowStore.getState()
|
||||
setRagPipelineVariables?.(rag_pipeline_variables)
|
||||
}
|
||||
if (environment_variables) {
|
||||
const { setEnvironmentVariables } = workflowStore.getState()
|
||||
setEnvironmentVariables(environment_variables)
|
||||
}
|
||||
}, [workflowStore])
|
||||
|
||||
const {
|
||||
doSyncWorkflowDraft,
|
||||
syncWorkflowDraftWhenPageClose,
|
||||
} = useNodesSyncDraft()
|
||||
const { handleRefreshWorkflowDraft } = usePipelineRefreshDraft()
|
||||
const {
|
||||
handleBackupDraft,
|
||||
handleLoadBackupDraft,
|
||||
handleRestoreFromPublishedWorkflow,
|
||||
handleRun,
|
||||
handleStopRun,
|
||||
} = usePipelineRun()
|
||||
const {
|
||||
handleStartWorkflowRun,
|
||||
handleWorkflowStartRunInWorkflow,
|
||||
} = usePipelineStartRun()
|
||||
const availableNodesMetaData = useAvailableNodesMetaData()
|
||||
const { getWorkflowRunAndTraceUrl } = useGetRunAndTraceUrl()
|
||||
const {
|
||||
exportCheck,
|
||||
handleExportDSL,
|
||||
} = useDSL()
|
||||
|
||||
const configsMap = useConfigsMap()
|
||||
const { fetchInspectVars } = useSetWorkflowVarsWithValue({
|
||||
...configsMap,
|
||||
})
|
||||
const {
|
||||
hasNodeInspectVars,
|
||||
hasSetInspectVar,
|
||||
fetchInspectVarValue,
|
||||
editInspectVarValue,
|
||||
renameInspectVarName,
|
||||
appendNodeInspectVars,
|
||||
deleteInspectVar,
|
||||
deleteNodeInspectorVars,
|
||||
deleteAllInspectorVars,
|
||||
isInspectVarEdited,
|
||||
resetToLastRunVar,
|
||||
invalidateSysVarValues,
|
||||
resetConversationVar,
|
||||
invalidateConversationVarValues,
|
||||
} = useInspectVarsCrud()
|
||||
|
||||
const hooksStore = useMemo(() => {
|
||||
return {
|
||||
availableNodesMetaData,
|
||||
syncWorkflowDraftWhenPageClose,
|
||||
doSyncWorkflowDraft,
|
||||
handleRefreshWorkflowDraft,
|
||||
handleBackupDraft,
|
||||
handleLoadBackupDraft,
|
||||
handleRestoreFromPublishedWorkflow,
|
||||
handleRun,
|
||||
handleStopRun,
|
||||
handleStartWorkflowRun,
|
||||
handleWorkflowStartRunInWorkflow,
|
||||
getWorkflowRunAndTraceUrl,
|
||||
exportCheck,
|
||||
handleExportDSL,
|
||||
fetchInspectVars,
|
||||
hasNodeInspectVars,
|
||||
hasSetInspectVar,
|
||||
fetchInspectVarValue,
|
||||
editInspectVarValue,
|
||||
renameInspectVarName,
|
||||
appendNodeInspectVars,
|
||||
deleteInspectVar,
|
||||
deleteNodeInspectorVars,
|
||||
deleteAllInspectorVars,
|
||||
isInspectVarEdited,
|
||||
resetToLastRunVar,
|
||||
invalidateSysVarValues,
|
||||
resetConversationVar,
|
||||
invalidateConversationVarValues,
|
||||
configsMap,
|
||||
}
|
||||
}, [
|
||||
availableNodesMetaData,
|
||||
syncWorkflowDraftWhenPageClose,
|
||||
doSyncWorkflowDraft,
|
||||
handleRefreshWorkflowDraft,
|
||||
handleBackupDraft,
|
||||
handleLoadBackupDraft,
|
||||
handleRestoreFromPublishedWorkflow,
|
||||
handleRun,
|
||||
handleStopRun,
|
||||
handleStartWorkflowRun,
|
||||
handleWorkflowStartRunInWorkflow,
|
||||
getWorkflowRunAndTraceUrl,
|
||||
exportCheck,
|
||||
handleExportDSL,
|
||||
fetchInspectVars,
|
||||
hasNodeInspectVars,
|
||||
hasSetInspectVar,
|
||||
fetchInspectVarValue,
|
||||
editInspectVarValue,
|
||||
renameInspectVarName,
|
||||
appendNodeInspectVars,
|
||||
deleteInspectVar,
|
||||
deleteNodeInspectorVars,
|
||||
deleteAllInspectorVars,
|
||||
isInspectVarEdited,
|
||||
resetToLastRunVar,
|
||||
invalidateSysVarValues,
|
||||
resetConversationVar,
|
||||
invalidateConversationVarValues,
|
||||
configsMap,
|
||||
])
|
||||
|
||||
return (
|
||||
<WorkflowWithInnerContext
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
viewport={viewport}
|
||||
hooksStore={hooksStore as any}
|
||||
onWorkflowDataUpdate={handleWorkflowDataUpdate}
|
||||
>
|
||||
<RagPipelineChildren />
|
||||
</WorkflowWithInnerContext>
|
||||
)
|
||||
}
|
||||
|
||||
export default RagPipelineMain
|
||||
@@ -0,0 +1,22 @@
|
||||
import React from 'react'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { basePath } from '@/utils/var'
|
||||
import Image from 'next/image'
|
||||
|
||||
const PipelineScreenShot = () => {
|
||||
const { theme } = useTheme()
|
||||
|
||||
return (
|
||||
<picture>
|
||||
<source media="(resolution: 1x)" srcSet={`${basePath}/screenshots/${theme}/Pipeline.png`} />
|
||||
<source media="(resolution: 2x)" srcSet={`${basePath}/screenshots/${theme}/Pipeline@2x.png`} />
|
||||
<source media="(resolution: 3x)" srcSet={`${basePath}/screenshots/${theme}/Pipeline@3x.png`} />
|
||||
<Image
|
||||
src={`${basePath}/screenshots/${theme}/Pipeline.png`}
|
||||
alt='Pipeline Screenshot'
|
||||
width={692} height={456} />
|
||||
</picture>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(PipelineScreenShot)
|
||||
@@ -0,0 +1,277 @@
|
||||
'use client'
|
||||
|
||||
import type { MouseEventHandler } from 'react'
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiAlertFill,
|
||||
RiCloseLine,
|
||||
RiFileDownloadLine,
|
||||
} from '@remixicon/react'
|
||||
import { WORKFLOW_DATA_UPDATE } from '@/app/components/workflow/constants'
|
||||
import {
|
||||
initialEdges,
|
||||
initialNodes,
|
||||
} from '@/app/components/workflow/utils'
|
||||
import {
|
||||
useImportPipelineDSL,
|
||||
useImportPipelineDSLConfirm,
|
||||
} from '@/service/use-pipeline'
|
||||
import { fetchWorkflowDraft } from '@/service/workflow'
|
||||
import {
|
||||
DSLImportMode,
|
||||
DSLImportStatus,
|
||||
} from '@/models/app'
|
||||
import Uploader from '@/app/components/app/create-from-dsl-modal/uploader'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks'
|
||||
import { useWorkflowStore } from '@/app/components/workflow/store'
|
||||
|
||||
type UpdateDSLModalProps = {
|
||||
onCancel: () => void
|
||||
onBackup: () => void
|
||||
onImport?: () => void
|
||||
}
|
||||
|
||||
const UpdateDSLModal = ({
|
||||
onCancel,
|
||||
onBackup,
|
||||
onImport,
|
||||
}: UpdateDSLModalProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useContext(ToastContext)
|
||||
const [currentFile, setDSLFile] = useState<File>()
|
||||
const [fileContent, setFileContent] = useState<string>()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
const [show, setShow] = useState(true)
|
||||
const [showErrorModal, setShowErrorModal] = useState(false)
|
||||
const [versions, setVersions] = useState<{ importedVersion: string; systemVersion: string }>()
|
||||
const [importId, setImportId] = useState<string>()
|
||||
const { handleCheckPluginDependencies } = usePluginDependencies()
|
||||
const { mutateAsync: importDSL } = useImportPipelineDSL()
|
||||
const { mutateAsync: importDSLConfirm } = useImportPipelineDSLConfirm()
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
const readFile = (file: File) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = function (event) {
|
||||
const content = event.target?.result
|
||||
setFileContent(content as string)
|
||||
}
|
||||
reader.readAsText(file)
|
||||
}
|
||||
|
||||
const handleFile = (file?: File) => {
|
||||
setDSLFile(file)
|
||||
if (file)
|
||||
readFile(file)
|
||||
if (!file)
|
||||
setFileContent('')
|
||||
}
|
||||
|
||||
const handleWorkflowUpdate = useCallback(async (pipelineId: string) => {
|
||||
const {
|
||||
graph,
|
||||
hash,
|
||||
rag_pipeline_variables,
|
||||
} = await fetchWorkflowDraft(`/rag/pipelines/${pipelineId}/workflows/draft`)
|
||||
|
||||
const { nodes, edges, viewport } = graph
|
||||
|
||||
eventEmitter?.emit({
|
||||
type: WORKFLOW_DATA_UPDATE,
|
||||
payload: {
|
||||
nodes: initialNodes(nodes, edges),
|
||||
edges: initialEdges(edges, nodes),
|
||||
viewport,
|
||||
hash,
|
||||
rag_pipeline_variables: rag_pipeline_variables || [],
|
||||
},
|
||||
} as any)
|
||||
}, [eventEmitter])
|
||||
|
||||
const isCreatingRef = useRef(false)
|
||||
const handleImport: MouseEventHandler = useCallback(async () => {
|
||||
const { pipelineId } = workflowStore.getState()
|
||||
if (isCreatingRef.current)
|
||||
return
|
||||
isCreatingRef.current = true
|
||||
if (!currentFile)
|
||||
return
|
||||
try {
|
||||
if (pipelineId && fileContent) {
|
||||
setLoading(true)
|
||||
const response = await importDSL({ mode: DSLImportMode.YAML_CONTENT, yaml_content: fileContent, pipeline_id: pipelineId })
|
||||
const { id, status, pipeline_id, imported_dsl_version, current_dsl_version } = response
|
||||
|
||||
if (status === DSLImportStatus.COMPLETED || status === DSLImportStatus.COMPLETED_WITH_WARNINGS) {
|
||||
if (!pipeline_id) {
|
||||
notify({ type: 'error', message: t('workflow.common.importFailure') })
|
||||
return
|
||||
}
|
||||
handleWorkflowUpdate(pipeline_id)
|
||||
if (onImport)
|
||||
onImport()
|
||||
notify({
|
||||
type: status === DSLImportStatus.COMPLETED ? 'success' : 'warning',
|
||||
message: t(status === DSLImportStatus.COMPLETED ? 'workflow.common.importSuccess' : 'workflow.common.importWarning'),
|
||||
children: status === DSLImportStatus.COMPLETED_WITH_WARNINGS && t('workflow.common.importWarningDetails'),
|
||||
})
|
||||
await handleCheckPluginDependencies(pipeline_id, true)
|
||||
setLoading(false)
|
||||
onCancel()
|
||||
}
|
||||
else if (status === DSLImportStatus.PENDING) {
|
||||
setShow(false)
|
||||
setTimeout(() => {
|
||||
setShowErrorModal(true)
|
||||
}, 300)
|
||||
setVersions({
|
||||
importedVersion: imported_dsl_version ?? '',
|
||||
systemVersion: current_dsl_version ?? '',
|
||||
})
|
||||
setImportId(id)
|
||||
}
|
||||
else {
|
||||
setLoading(false)
|
||||
notify({ type: 'error', message: t('workflow.common.importFailure') })
|
||||
}
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||
catch (e) {
|
||||
setLoading(false)
|
||||
notify({ type: 'error', message: t('workflow.common.importFailure') })
|
||||
}
|
||||
isCreatingRef.current = false
|
||||
}, [currentFile, fileContent, onCancel, notify, t, onImport, handleWorkflowUpdate, handleCheckPluginDependencies, workflowStore, importDSL])
|
||||
|
||||
const onUpdateDSLConfirm: MouseEventHandler = async () => {
|
||||
try {
|
||||
if (!importId)
|
||||
return
|
||||
const response = await importDSLConfirm(importId)
|
||||
|
||||
const { status, pipeline_id } = response
|
||||
|
||||
if (status === DSLImportStatus.COMPLETED) {
|
||||
if (!pipeline_id) {
|
||||
notify({ type: 'error', message: t('workflow.common.importFailure') })
|
||||
return
|
||||
}
|
||||
handleWorkflowUpdate(pipeline_id)
|
||||
await handleCheckPluginDependencies(pipeline_id, true)
|
||||
if (onImport)
|
||||
onImport()
|
||||
notify({ type: 'success', message: t('workflow.common.importSuccess') })
|
||||
setLoading(false)
|
||||
onCancel()
|
||||
}
|
||||
else if (status === DSLImportStatus.FAILED) {
|
||||
setLoading(false)
|
||||
notify({ type: 'error', message: t('workflow.common.importFailure') })
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars
|
||||
catch (e) {
|
||||
setLoading(false)
|
||||
notify({ type: 'error', message: t('workflow.common.importFailure') })
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
className='w-[520px] rounded-2xl p-6'
|
||||
isShow={show}
|
||||
onClose={onCancel}
|
||||
>
|
||||
<div className='mb-3 flex items-center justify-between'>
|
||||
<div className='title-2xl-semi-bold text-text-primary'>{t('workflow.common.importDSL')}</div>
|
||||
<div className='flex h-[22px] w-[22px] cursor-pointer items-center justify-center' onClick={onCancel}>
|
||||
<RiCloseLine className='h-[18px] w-[18px] text-text-tertiary' />
|
||||
</div>
|
||||
</div>
|
||||
<div className='relative mb-2 flex grow gap-0.5 overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-2 shadow-xs'>
|
||||
<div className='absolute left-0 top-0 h-full w-full bg-toast-warning-bg opacity-40' />
|
||||
<div className='flex items-start justify-center p-1'>
|
||||
<RiAlertFill className='h-4 w-4 shrink-0 text-text-warning-secondary' />
|
||||
</div>
|
||||
<div className='flex grow flex-col items-start gap-0.5 py-1'>
|
||||
<div className='system-xs-medium whitespace-pre-line text-text-primary'>{t('workflow.common.importDSLTip')}</div>
|
||||
<div className='flex items-start gap-1 self-stretch pb-0.5 pt-1'>
|
||||
<Button
|
||||
size='small'
|
||||
variant='secondary'
|
||||
className='z-[1000]'
|
||||
onClick={onBackup}
|
||||
>
|
||||
<RiFileDownloadLine className='h-3.5 w-3.5 text-components-button-secondary-text' />
|
||||
<div className='flex items-center justify-center gap-1 px-[3px]'>
|
||||
{t('workflow.common.backupCurrentDraft')}
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className='system-md-semibold pt-2 text-text-primary'>
|
||||
{t('workflow.common.chooseDSL')}
|
||||
</div>
|
||||
<div className='flex w-full flex-col items-start justify-center gap-4 self-stretch py-4'>
|
||||
<Uploader
|
||||
file={currentFile}
|
||||
updateFile={handleFile}
|
||||
className='!mt-0 w-full'
|
||||
accept='.pipeline'
|
||||
displayName='PIPELINE'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-center justify-end gap-2 self-stretch pt-5'>
|
||||
<Button onClick={onCancel}>{t('app.newApp.Cancel')}</Button>
|
||||
<Button
|
||||
disabled={!currentFile || loading}
|
||||
variant='warning'
|
||||
onClick={handleImport}
|
||||
loading={loading}
|
||||
>
|
||||
{t('workflow.common.overwriteAndImport')}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
<Modal
|
||||
isShow={showErrorModal}
|
||||
onClose={() => setShowErrorModal(false)}
|
||||
className='w-[480px]'
|
||||
>
|
||||
<div className='flex flex-col items-start gap-2 self-stretch pb-4'>
|
||||
<div className='title-2xl-semi-bold text-text-primary'>{t('app.newApp.appCreateDSLErrorTitle')}</div>
|
||||
<div className='system-md-regular flex grow flex-col text-text-secondary'>
|
||||
<div>{t('app.newApp.appCreateDSLErrorPart1')}</div>
|
||||
<div>{t('app.newApp.appCreateDSLErrorPart2')}</div>
|
||||
<br />
|
||||
<div>{t('app.newApp.appCreateDSLErrorPart3')}<span className='system-md-medium'>{versions?.importedVersion}</span></div>
|
||||
<div>{t('app.newApp.appCreateDSLErrorPart4')}<span className='system-md-medium'>{versions?.systemVersion}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-start justify-end gap-2 self-stretch pt-6'>
|
||||
<Button variant='secondary' onClick={() => setShowErrorModal(false)}>{t('app.newApp.Cancel')}</Button>
|
||||
<Button variant='primary' destructive onClick={onUpdateDSLConfirm}>{t('app.newApp.Confirm')}</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(UpdateDSLModal)
|
||||
9
dify/web/app/components/rag-pipeline/hooks/index.ts
Normal file
9
dify/web/app/components/rag-pipeline/hooks/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export * from './use-available-nodes-meta-data'
|
||||
export * from './use-pipeline-refresh-draft'
|
||||
export * from './use-nodes-sync-draft'
|
||||
export * from './use-pipeline-run'
|
||||
export * from './use-pipeline-start-run'
|
||||
export * from './use-pipeline-init'
|
||||
export * from './use-get-run-and-trace-url'
|
||||
export * from './use-DSL'
|
||||
export * from './use-input-field-panel'
|
||||
83
dify/web/app/components/rag-pipeline/hooks/use-DSL.ts
Normal file
83
dify/web/app/components/rag-pipeline/hooks/use-DSL.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import {
|
||||
useCallback,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
DSL_EXPORT_CHECK,
|
||||
} from '@/app/components/workflow/constants'
|
||||
import { useNodesSyncDraft } from './use-nodes-sync-draft'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { fetchWorkflowDraft } from '@/service/workflow'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import { useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import { useExportPipelineDSL } from '@/service/use-pipeline'
|
||||
|
||||
export const useDSL = () => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useToastContext()
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
const [exporting, setExporting] = useState(false)
|
||||
const { doSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const { mutateAsync: exportPipelineConfig } = useExportPipelineDSL()
|
||||
|
||||
const handleExportDSL = useCallback(async (include = false) => {
|
||||
const { pipelineId, knowledgeName } = workflowStore.getState()
|
||||
if (!pipelineId)
|
||||
return
|
||||
|
||||
if (exporting)
|
||||
return
|
||||
|
||||
try {
|
||||
setExporting(true)
|
||||
await doSyncWorkflowDraft()
|
||||
const { data } = await exportPipelineConfig({
|
||||
pipelineId,
|
||||
include,
|
||||
})
|
||||
const a = document.createElement('a')
|
||||
const file = new Blob([data], { type: 'application/yaml' })
|
||||
const url = URL.createObjectURL(file)
|
||||
a.href = url
|
||||
a.download = `${knowledgeName}.pipeline`
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
catch {
|
||||
notify({ type: 'error', message: t('app.exportFailed') })
|
||||
}
|
||||
finally {
|
||||
setExporting(false)
|
||||
}
|
||||
}, [notify, t, doSyncWorkflowDraft, exporting, exportPipelineConfig, workflowStore])
|
||||
|
||||
const exportCheck = useCallback(async () => {
|
||||
const { pipelineId } = workflowStore.getState()
|
||||
if (!pipelineId)
|
||||
return
|
||||
try {
|
||||
const workflowDraft = await fetchWorkflowDraft(`/rag/pipelines/${pipelineId}/workflows/draft`)
|
||||
const list = (workflowDraft.environment_variables || []).filter(env => env.value_type === 'secret')
|
||||
if (list.length === 0) {
|
||||
handleExportDSL()
|
||||
return
|
||||
}
|
||||
eventEmitter?.emit({
|
||||
type: DSL_EXPORT_CHECK,
|
||||
payload: {
|
||||
data: list,
|
||||
},
|
||||
} as any)
|
||||
}
|
||||
catch {
|
||||
notify({ type: 'error', message: t('app.exportFailed') })
|
||||
}
|
||||
}, [eventEmitter, handleExportDSL, notify, t, workflowStore])
|
||||
|
||||
return {
|
||||
exportCheck,
|
||||
handleExportDSL,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import knowledgeBaseDefault from '@/app/components/workflow/nodes/knowledge-base/default'
|
||||
import dataSourceDefault from '@/app/components/workflow/nodes/data-source/default'
|
||||
import dataSourceEmptyDefault from '@/app/components/workflow/nodes/data-source-empty/default'
|
||||
import { WORKFLOW_COMMON_NODES } from '@/app/components/workflow/constants/node'
|
||||
import type { AvailableNodesMetaData } from '@/app/components/workflow/hooks-store/store'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
|
||||
export const useAvailableNodesMetaData = () => {
|
||||
const { t } = useTranslation()
|
||||
const language = useGetLanguage()
|
||||
|
||||
const mergedNodesMetaData = useMemo(() => [
|
||||
...WORKFLOW_COMMON_NODES,
|
||||
{
|
||||
...dataSourceDefault,
|
||||
defaultValue: {
|
||||
...dataSourceDefault.defaultValue,
|
||||
_dataSourceStartToAdd: true,
|
||||
},
|
||||
},
|
||||
knowledgeBaseDefault,
|
||||
dataSourceEmptyDefault,
|
||||
], [])
|
||||
|
||||
const helpLinkUri = useMemo(() => {
|
||||
if (language === 'zh_Hans')
|
||||
return 'https://docs.dify.ai/zh-hans/guides/knowledge-base/knowledge-pipeline/knowledge-pipeline-orchestration#%E6%AD%A5%E9%AA%A4%E4%B8%80%EF%BC%9A%E6%95%B0%E6%8D%AE%E6%BA%90%E9%85%8D%E7%BD%AE'
|
||||
if (language === 'ja_JP')
|
||||
return 'https://docs.dify.ai/ja-jp/guides/knowledge-base/knowledge-pipeline/knowledge-pipeline-orchestration#%E3%82%B9%E3%83%86%E3%83%83%E3%83%971%EF%BC%9A%E3%83%87%E3%83%BC%E3%82%BF%E3%82%BD%E3%83%BC%E3%82%B9%E3%81%AE%E8%A8%AD%E5%AE%9A'
|
||||
|
||||
return 'https://docs.dify.ai/en/guides/knowledge-base/knowledge-pipeline/knowledge-pipeline-orchestration#step-1%3A-data-source'
|
||||
}, [language])
|
||||
|
||||
const availableNodesMetaData = useMemo(() => mergedNodesMetaData.map((node) => {
|
||||
const { metaData } = node
|
||||
const title = t(`workflow.blocks.${metaData.type}`)
|
||||
const description = t(`workflow.blocksAbout.${metaData.type}`)
|
||||
return {
|
||||
...node,
|
||||
metaData: {
|
||||
...metaData,
|
||||
title,
|
||||
description,
|
||||
helpLinkUri,
|
||||
},
|
||||
defaultValue: {
|
||||
...node.defaultValue,
|
||||
type: metaData.type,
|
||||
title,
|
||||
},
|
||||
}
|
||||
}), [mergedNodesMetaData, t])
|
||||
|
||||
const availableNodesMetaDataMap = useMemo(() => availableNodesMetaData.reduce((acc, node) => {
|
||||
acc![node.metaData.type] = node
|
||||
return acc
|
||||
}, {} as AvailableNodesMetaData['nodesMap']), [availableNodesMetaData])
|
||||
|
||||
return useMemo(() => {
|
||||
return {
|
||||
nodes: availableNodesMetaData,
|
||||
nodesMap: {
|
||||
...availableNodesMetaDataMap,
|
||||
[BlockEnum.VariableAssigner]: availableNodesMetaDataMap?.[BlockEnum.VariableAggregator],
|
||||
},
|
||||
}
|
||||
}, [availableNodesMetaData, availableNodesMetaDataMap])
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { useMemo } from 'react'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import { FlowType } from '@/types/common'
|
||||
import { Resolution, TransferMethod } from '@/types/app'
|
||||
|
||||
export const useConfigsMap = () => {
|
||||
const pipelineId = useStore(s => s.pipelineId)
|
||||
const fileUploadConfig = useStore(s => s.fileUploadConfig)
|
||||
return useMemo(() => {
|
||||
return {
|
||||
flowId: pipelineId!,
|
||||
flowType: FlowType.ragPipeline,
|
||||
fileSettings: {
|
||||
image: {
|
||||
enabled: false,
|
||||
detail: Resolution.high,
|
||||
number_limits: 3,
|
||||
transfer_methods: [TransferMethod.local_file, TransferMethod.remote_url],
|
||||
},
|
||||
fileUploadConfig,
|
||||
},
|
||||
}
|
||||
}, [pipelineId])
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { useCallback } from 'react'
|
||||
import { useWorkflowStore } from '@/app/components/workflow/store'
|
||||
|
||||
export const useGetRunAndTraceUrl = () => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const getWorkflowRunAndTraceUrl = useCallback((runId: string) => {
|
||||
const { pipelineId } = workflowStore.getState()
|
||||
|
||||
return {
|
||||
runUrl: `/rag/pipelines/${pipelineId}/workflow-runs/${runId}`,
|
||||
traceUrl: `/rag/pipelines/${pipelineId}/workflow-runs/${runId}/node-executions`,
|
||||
}
|
||||
}, [workflowStore])
|
||||
|
||||
return {
|
||||
getWorkflowRunAndTraceUrl,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useStore, useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import type { InputFieldEditorProps } from '../components/panel/input-field/editor'
|
||||
|
||||
export const useInputFieldPanel = () => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const showInputFieldPreviewPanel = useStore(state => state.showInputFieldPreviewPanel)
|
||||
const inputFieldEditPanelProps = useStore(state => state.inputFieldEditPanelProps)
|
||||
|
||||
const isPreviewing = useMemo(() => {
|
||||
return showInputFieldPreviewPanel
|
||||
}, [showInputFieldPreviewPanel])
|
||||
|
||||
const isEditing = useMemo(() => {
|
||||
return !!inputFieldEditPanelProps
|
||||
}, [inputFieldEditPanelProps])
|
||||
|
||||
const closeAllInputFieldPanels = useCallback(() => {
|
||||
const {
|
||||
setShowInputFieldPanel,
|
||||
setShowInputFieldPreviewPanel,
|
||||
setInputFieldEditPanelProps,
|
||||
} = workflowStore.getState()
|
||||
|
||||
setShowInputFieldPanel?.(false)
|
||||
setShowInputFieldPreviewPanel?.(false)
|
||||
setInputFieldEditPanelProps?.(null)
|
||||
}, [workflowStore])
|
||||
|
||||
const toggleInputFieldPreviewPanel = useCallback(() => {
|
||||
const {
|
||||
showInputFieldPreviewPanel,
|
||||
setShowInputFieldPreviewPanel,
|
||||
} = workflowStore.getState()
|
||||
|
||||
setShowInputFieldPreviewPanel?.(!showInputFieldPreviewPanel)
|
||||
}, [workflowStore])
|
||||
|
||||
const toggleInputFieldEditPanel = useCallback((editContent: InputFieldEditorProps | null) => {
|
||||
const {
|
||||
setInputFieldEditPanelProps,
|
||||
} = workflowStore.getState()
|
||||
|
||||
setInputFieldEditPanelProps?.(editContent)
|
||||
}, [workflowStore])
|
||||
|
||||
return {
|
||||
closeAllInputFieldPanels,
|
||||
toggleInputFieldPreviewPanel,
|
||||
toggleInputFieldEditPanel,
|
||||
isPreviewing,
|
||||
isEditing,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { useMemo } from 'react'
|
||||
import type { BaseConfiguration } from '@/app/components/base/form/form-scenarios/base/types'
|
||||
import { type RAGPipelineVariables, VAR_TYPE_MAP } from '@/models/pipeline'
|
||||
import { BaseFieldType } from '@/app/components/base/form/form-scenarios/base/types'
|
||||
|
||||
export const useInitialData = (variables: RAGPipelineVariables, lastRunInputData?: Record<string, any>) => {
|
||||
const initialData = useMemo(() => {
|
||||
return variables.reduce((acc, item) => {
|
||||
const type = VAR_TYPE_MAP[item.type]
|
||||
const variableName = item.variable
|
||||
const defaultValue = lastRunInputData?.[variableName] || item.default_value
|
||||
if ([BaseFieldType.textInput, BaseFieldType.paragraph, BaseFieldType.select].includes(type))
|
||||
acc[variableName] = defaultValue ?? ''
|
||||
if (type === BaseFieldType.numberInput)
|
||||
acc[variableName] = defaultValue ?? 0
|
||||
if (type === BaseFieldType.checkbox)
|
||||
acc[variableName] = defaultValue ?? false
|
||||
if ([BaseFieldType.file, BaseFieldType.fileList].includes(type))
|
||||
acc[variableName] = defaultValue ?? []
|
||||
return acc
|
||||
}, {} as Record<string, any>)
|
||||
}, [lastRunInputData, variables])
|
||||
|
||||
return initialData
|
||||
}
|
||||
|
||||
export const useConfigurations = (variables: RAGPipelineVariables) => {
|
||||
const configurations = useMemo(() => {
|
||||
const configurations: BaseConfiguration[] = []
|
||||
variables.forEach((item) => {
|
||||
configurations.push({
|
||||
type: VAR_TYPE_MAP[item.type],
|
||||
variable: item.variable,
|
||||
label: item.label,
|
||||
required: item.required,
|
||||
maxLength: item.max_length,
|
||||
options: item.options?.map(option => ({
|
||||
label: option,
|
||||
value: option,
|
||||
})),
|
||||
showConditions: [],
|
||||
placeholder: item.placeholder,
|
||||
tooltip: item.tooltips,
|
||||
unit: item.unit,
|
||||
allowedFileTypes: item.allowed_file_types,
|
||||
allowedFileExtensions: item.allowed_file_extensions,
|
||||
allowedFileUploadMethods: item.allowed_file_upload_methods,
|
||||
})
|
||||
})
|
||||
return configurations
|
||||
}, [variables])
|
||||
|
||||
return configurations
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { useInspectVarsCrudCommon } from '../../workflow/hooks/use-inspect-vars-crud-common'
|
||||
import { useConfigsMap } from './use-configs-map'
|
||||
|
||||
export const useInspectVarsCrud = () => {
|
||||
const configsMap = useConfigsMap()
|
||||
const apis = useInspectVarsCrudCommon({
|
||||
...configsMap,
|
||||
})
|
||||
|
||||
return {
|
||||
...apis,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import { useCallback } from 'react'
|
||||
import { produce } from 'immer'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import {
|
||||
useWorkflowStore,
|
||||
} from '@/app/components/workflow/store'
|
||||
import {
|
||||
useNodesReadOnly,
|
||||
} from '@/app/components/workflow/hooks/use-workflow'
|
||||
import { useSerialAsyncCallback } from '@/app/components/workflow/hooks/use-serial-async-callback'
|
||||
import { API_PREFIX } from '@/config'
|
||||
import { syncWorkflowDraft } from '@/service/workflow'
|
||||
import { usePipelineRefreshDraft } from '.'
|
||||
|
||||
export const useNodesSyncDraft = () => {
|
||||
const store = useStoreApi()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const { getNodesReadOnly } = useNodesReadOnly()
|
||||
const { handleRefreshWorkflowDraft } = usePipelineRefreshDraft()
|
||||
|
||||
const getPostParams = useCallback(() => {
|
||||
const {
|
||||
getNodes,
|
||||
edges,
|
||||
transform,
|
||||
} = store.getState()
|
||||
const nodesOriginal = getNodes()
|
||||
const nodes = nodesOriginal.filter(node => !node.data._isTempNode)
|
||||
const [x, y, zoom] = transform
|
||||
const {
|
||||
pipelineId,
|
||||
environmentVariables,
|
||||
syncWorkflowDraftHash,
|
||||
ragPipelineVariables,
|
||||
} = workflowStore.getState()
|
||||
|
||||
if (pipelineId && !!nodes.length) {
|
||||
const producedNodes = produce(nodes, (draft) => {
|
||||
draft.forEach((node) => {
|
||||
Object.keys(node.data).forEach((key) => {
|
||||
if (key.startsWith('_'))
|
||||
delete node.data[key]
|
||||
})
|
||||
})
|
||||
})
|
||||
const producedEdges = produce(edges, (draft) => {
|
||||
draft.forEach((edge) => {
|
||||
Object.keys(edge.data).forEach((key) => {
|
||||
if (key.startsWith('_'))
|
||||
delete edge.data[key]
|
||||
})
|
||||
})
|
||||
})
|
||||
return {
|
||||
url: `/rag/pipelines/${pipelineId}/workflows/draft`,
|
||||
params: {
|
||||
graph: {
|
||||
nodes: producedNodes,
|
||||
edges: producedEdges,
|
||||
viewport: {
|
||||
x,
|
||||
y,
|
||||
zoom,
|
||||
},
|
||||
},
|
||||
environment_variables: environmentVariables,
|
||||
rag_pipeline_variables: ragPipelineVariables,
|
||||
hash: syncWorkflowDraftHash,
|
||||
},
|
||||
}
|
||||
}
|
||||
}, [store, workflowStore])
|
||||
|
||||
const syncWorkflowDraftWhenPageClose = useCallback(() => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
const postParams = getPostParams()
|
||||
|
||||
if (postParams) {
|
||||
navigator.sendBeacon(
|
||||
`${API_PREFIX}${postParams.url}`,
|
||||
JSON.stringify(postParams.params),
|
||||
)
|
||||
}
|
||||
}, [getPostParams, getNodesReadOnly])
|
||||
|
||||
const performSync = useCallback(async (
|
||||
notRefreshWhenSyncError?: boolean,
|
||||
callback?: {
|
||||
onSuccess?: () => void
|
||||
onError?: () => void
|
||||
onSettled?: () => void
|
||||
},
|
||||
) => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
const postParams = getPostParams()
|
||||
if (postParams) {
|
||||
const {
|
||||
setSyncWorkflowDraftHash,
|
||||
setDraftUpdatedAt,
|
||||
} = workflowStore.getState()
|
||||
try {
|
||||
const res = await syncWorkflowDraft(postParams)
|
||||
setSyncWorkflowDraftHash(res.hash)
|
||||
setDraftUpdatedAt(res.updated_at)
|
||||
callback?.onSuccess?.()
|
||||
}
|
||||
catch (error: any) {
|
||||
if (error && error.json && !error.bodyUsed) {
|
||||
error.json().then((err: any) => {
|
||||
if (err.code === 'draft_workflow_not_sync' && !notRefreshWhenSyncError)
|
||||
handleRefreshWorkflowDraft()
|
||||
})
|
||||
}
|
||||
callback?.onError?.()
|
||||
}
|
||||
finally {
|
||||
callback?.onSettled?.()
|
||||
}
|
||||
}
|
||||
}, [getPostParams, getNodesReadOnly, workflowStore, handleRefreshWorkflowDraft])
|
||||
|
||||
const doSyncWorkflowDraft = useSerialAsyncCallback(performSync, getNodesReadOnly)
|
||||
|
||||
return {
|
||||
doSyncWorkflowDraft,
|
||||
syncWorkflowDraftWhenPageClose,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { useCallback } from 'react'
|
||||
import {
|
||||
useStore,
|
||||
useWorkflowStore,
|
||||
} from '@/app/components/workflow/store'
|
||||
import { useWorkflowConfig } from '@/service/use-workflow'
|
||||
import type { FetchWorkflowDraftResponse } from '@/types/workflow'
|
||||
import { useDataSourceList } from '@/service/use-pipeline'
|
||||
import type { DataSourceItem } from '@/app/components/workflow/block-selector/types'
|
||||
import { basePath } from '@/utils/var'
|
||||
import type { FileUploadConfigResponse } from '@/models/common'
|
||||
|
||||
export const usePipelineConfig = () => {
|
||||
const pipelineId = useStore(s => s.pipelineId)
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
const handleUpdateNodesDefaultConfigs = useCallback((nodesDefaultConfigs: Record<string, any> | Record<string, any>[]) => {
|
||||
const { setNodesDefaultConfigs } = workflowStore.getState()
|
||||
let res: Record<string, any> = {}
|
||||
if (Array.isArray(nodesDefaultConfigs)) {
|
||||
nodesDefaultConfigs.forEach((item) => {
|
||||
res[item.type] = item.config
|
||||
})
|
||||
}
|
||||
else {
|
||||
res = nodesDefaultConfigs as Record<string, any>
|
||||
}
|
||||
|
||||
setNodesDefaultConfigs!(res)
|
||||
}, [workflowStore])
|
||||
useWorkflowConfig(
|
||||
pipelineId ? `/rag/pipelines/${pipelineId}/workflows/default-workflow-block-configs` : '',
|
||||
handleUpdateNodesDefaultConfigs,
|
||||
)
|
||||
|
||||
const handleUpdatePublishedAt = useCallback((publishedWorkflow: FetchWorkflowDraftResponse) => {
|
||||
const { setPublishedAt } = workflowStore.getState()
|
||||
|
||||
setPublishedAt(publishedWorkflow?.created_at)
|
||||
}, [workflowStore])
|
||||
useWorkflowConfig(
|
||||
pipelineId ? `/rag/pipelines/${pipelineId}/workflows/publish` : '',
|
||||
handleUpdatePublishedAt,
|
||||
)
|
||||
|
||||
const handleUpdateDataSourceList = useCallback((dataSourceList: DataSourceItem[]) => {
|
||||
dataSourceList.forEach((item) => {
|
||||
const icon = item.declaration.identity.icon
|
||||
if (typeof icon == 'string' && !icon.includes(basePath))
|
||||
item.declaration.identity.icon = `${basePath}${icon}`
|
||||
})
|
||||
const { setDataSourceList } = workflowStore.getState()
|
||||
setDataSourceList!(dataSourceList)
|
||||
}, [workflowStore])
|
||||
|
||||
const handleUpdateWorkflowFileUploadConfig = useCallback((config: FileUploadConfigResponse) => {
|
||||
const { setFileUploadConfig } = workflowStore.getState()
|
||||
setFileUploadConfig(config)
|
||||
}, [workflowStore])
|
||||
useWorkflowConfig('/files/upload', handleUpdateWorkflowFileUploadConfig)
|
||||
|
||||
useDataSourceList(!!pipelineId, handleUpdateDataSourceList)
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react'
|
||||
import {
|
||||
useWorkflowStore,
|
||||
} from '@/app/components/workflow/store'
|
||||
import { usePipelineTemplate } from './use-pipeline-template'
|
||||
import {
|
||||
fetchWorkflowDraft,
|
||||
syncWorkflowDraft,
|
||||
} from '@/service/workflow'
|
||||
import type { FetchWorkflowDraftResponse } from '@/types/workflow'
|
||||
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
|
||||
import { usePipelineConfig } from './use-pipeline-config'
|
||||
|
||||
export const usePipelineInit = () => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const {
|
||||
nodes: nodesTemplate,
|
||||
edges: edgesTemplate,
|
||||
} = usePipelineTemplate()
|
||||
const [data, setData] = useState<FetchWorkflowDraftResponse>()
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const datasetId = useDatasetDetailContextWithSelector(s => s.dataset)?.pipeline_id
|
||||
const knowledgeName = useDatasetDetailContextWithSelector(s => s.dataset)?.name
|
||||
const knowledgeIcon = useDatasetDetailContextWithSelector(s => s.dataset)?.icon_info
|
||||
|
||||
useEffect(() => {
|
||||
workflowStore.setState({ pipelineId: datasetId, knowledgeName, knowledgeIcon })
|
||||
}, [datasetId, workflowStore, knowledgeName, knowledgeIcon])
|
||||
|
||||
usePipelineConfig()
|
||||
|
||||
const handleGetInitialWorkflowData = useCallback(async () => {
|
||||
const {
|
||||
setEnvSecrets,
|
||||
setEnvironmentVariables,
|
||||
setSyncWorkflowDraftHash,
|
||||
setDraftUpdatedAt,
|
||||
setToolPublished,
|
||||
setRagPipelineVariables,
|
||||
} = workflowStore.getState()
|
||||
try {
|
||||
const res = await fetchWorkflowDraft(`/rag/pipelines/${datasetId}/workflows/draft`)
|
||||
setData(res)
|
||||
setDraftUpdatedAt(res.updated_at)
|
||||
setToolPublished(res.tool_published)
|
||||
setEnvSecrets((res.environment_variables || []).filter(env => env.value_type === 'secret').reduce((acc, env) => {
|
||||
acc[env.id] = env.value
|
||||
return acc
|
||||
}, {} as Record<string, string>))
|
||||
setEnvironmentVariables(res.environment_variables?.map(env => env.value_type === 'secret' ? { ...env, value: '[__HIDDEN__]' } : env) || [])
|
||||
setSyncWorkflowDraftHash(res.hash)
|
||||
setRagPipelineVariables?.(res.rag_pipeline_variables || [])
|
||||
setIsLoading(false)
|
||||
}
|
||||
catch (error: any) {
|
||||
if (error && error.json && !error.bodyUsed && datasetId) {
|
||||
error.json().then((err: any) => {
|
||||
if (err.code === 'draft_workflow_not_exist') {
|
||||
workflowStore.setState({
|
||||
notInitialWorkflow: true,
|
||||
shouldAutoOpenStartNodeSelector: true,
|
||||
})
|
||||
syncWorkflowDraft({
|
||||
url: `/rag/pipelines/${datasetId}/workflows/draft`,
|
||||
params: {
|
||||
graph: {
|
||||
nodes: nodesTemplate,
|
||||
edges: edgesTemplate,
|
||||
},
|
||||
environment_variables: [],
|
||||
},
|
||||
}).then((res) => {
|
||||
const { setDraftUpdatedAt } = workflowStore.getState()
|
||||
setDraftUpdatedAt(res.updated_at)
|
||||
handleGetInitialWorkflowData()
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}, [nodesTemplate, edgesTemplate, workflowStore, datasetId])
|
||||
|
||||
useEffect(() => {
|
||||
handleGetInitialWorkflowData()
|
||||
}, [])
|
||||
|
||||
return {
|
||||
data,
|
||||
isLoading,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { useCallback } from 'react'
|
||||
import { useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import { fetchWorkflowDraft } from '@/service/workflow'
|
||||
import type { WorkflowDataUpdater } from '@/app/components/workflow/types'
|
||||
import { useWorkflowUpdate } from '@/app/components/workflow/hooks'
|
||||
import { processNodesWithoutDataSource } from '../utils'
|
||||
|
||||
export const usePipelineRefreshDraft = () => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const { handleUpdateWorkflowCanvas } = useWorkflowUpdate()
|
||||
|
||||
const handleRefreshWorkflowDraft = useCallback(() => {
|
||||
const {
|
||||
pipelineId,
|
||||
setSyncWorkflowDraftHash,
|
||||
setIsSyncingWorkflowDraft,
|
||||
setEnvironmentVariables,
|
||||
setEnvSecrets,
|
||||
} = workflowStore.getState()
|
||||
setIsSyncingWorkflowDraft(true)
|
||||
fetchWorkflowDraft(`/rag/pipelines/${pipelineId}/workflows/draft`).then((response) => {
|
||||
const {
|
||||
nodes: processedNodes,
|
||||
viewport,
|
||||
} = processNodesWithoutDataSource(response.graph.nodes, response.graph.viewport)
|
||||
handleUpdateWorkflowCanvas({
|
||||
...response.graph,
|
||||
nodes: processedNodes,
|
||||
viewport,
|
||||
} as WorkflowDataUpdater)
|
||||
setSyncWorkflowDraftHash(response.hash)
|
||||
setEnvSecrets((response.environment_variables || []).filter(env => env.value_type === 'secret').reduce((acc, env) => {
|
||||
acc[env.id] = env.value
|
||||
return acc
|
||||
}, {} as Record<string, string>))
|
||||
setEnvironmentVariables(response.environment_variables?.map(env => env.value_type === 'secret' ? { ...env, value: '[__HIDDEN__]' } : env) || [])
|
||||
}).finally(() => setIsSyncingWorkflowDraft(false))
|
||||
}, [handleUpdateWorkflowCanvas, workflowStore])
|
||||
|
||||
return {
|
||||
handleRefreshWorkflowDraft,
|
||||
}
|
||||
}
|
||||
319
dify/web/app/components/rag-pipeline/hooks/use-pipeline-run.ts
Normal file
319
dify/web/app/components/rag-pipeline/hooks/use-pipeline-run.ts
Normal file
@@ -0,0 +1,319 @@
|
||||
import { useCallback } from 'react'
|
||||
import {
|
||||
useReactFlow,
|
||||
useStoreApi,
|
||||
} from 'reactflow'
|
||||
import { produce } from 'immer'
|
||||
import { useStore, useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import { WorkflowRunningStatus } from '@/app/components/workflow/types'
|
||||
import { useWorkflowUpdate } from '@/app/components/workflow/hooks/use-workflow-interactions'
|
||||
import { useWorkflowRunEvent } from '@/app/components/workflow/hooks/use-workflow-run-event/use-workflow-run-event'
|
||||
import type { IOtherOptions } from '@/service/base'
|
||||
import { ssePost } from '@/service/base'
|
||||
import { stopWorkflowRun } from '@/service/workflow'
|
||||
import type { VersionHistory } from '@/types/workflow'
|
||||
import { useNodesSyncDraft } from './use-nodes-sync-draft'
|
||||
import { useSetWorkflowVarsWithValue } from '@/app/components/workflow/hooks/use-fetch-workflow-inspect-vars'
|
||||
import { useInvalidAllLastRun } from '@/service/use-workflow'
|
||||
import { FlowType } from '@/types/common'
|
||||
|
||||
export const usePipelineRun = () => {
|
||||
const store = useStoreApi()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const reactflow = useReactFlow()
|
||||
const { doSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
const { handleUpdateWorkflowCanvas } = useWorkflowUpdate()
|
||||
|
||||
const {
|
||||
handleWorkflowStarted,
|
||||
handleWorkflowFinished,
|
||||
handleWorkflowFailed,
|
||||
handleWorkflowNodeStarted,
|
||||
handleWorkflowNodeFinished,
|
||||
handleWorkflowNodeIterationStarted,
|
||||
handleWorkflowNodeIterationNext,
|
||||
handleWorkflowNodeIterationFinished,
|
||||
handleWorkflowNodeLoopStarted,
|
||||
handleWorkflowNodeLoopNext,
|
||||
handleWorkflowNodeLoopFinished,
|
||||
handleWorkflowNodeRetry,
|
||||
handleWorkflowAgentLog,
|
||||
handleWorkflowTextChunk,
|
||||
handleWorkflowTextReplace,
|
||||
} = useWorkflowRunEvent()
|
||||
|
||||
const handleBackupDraft = useCallback(() => {
|
||||
const {
|
||||
getNodes,
|
||||
edges,
|
||||
} = store.getState()
|
||||
const { getViewport } = reactflow
|
||||
const {
|
||||
backupDraft,
|
||||
setBackupDraft,
|
||||
environmentVariables,
|
||||
} = workflowStore.getState()
|
||||
|
||||
if (!backupDraft) {
|
||||
setBackupDraft({
|
||||
nodes: getNodes(),
|
||||
edges,
|
||||
viewport: getViewport(),
|
||||
environmentVariables,
|
||||
})
|
||||
doSyncWorkflowDraft()
|
||||
}
|
||||
}, [reactflow, workflowStore, store, doSyncWorkflowDraft])
|
||||
|
||||
const handleLoadBackupDraft = useCallback(() => {
|
||||
const {
|
||||
backupDraft,
|
||||
setBackupDraft,
|
||||
setEnvironmentVariables,
|
||||
} = workflowStore.getState()
|
||||
|
||||
if (backupDraft) {
|
||||
const {
|
||||
nodes,
|
||||
edges,
|
||||
viewport,
|
||||
environmentVariables,
|
||||
} = backupDraft
|
||||
handleUpdateWorkflowCanvas({
|
||||
nodes,
|
||||
edges,
|
||||
viewport,
|
||||
})
|
||||
setEnvironmentVariables(environmentVariables)
|
||||
setBackupDraft(undefined)
|
||||
}
|
||||
}, [handleUpdateWorkflowCanvas, workflowStore])
|
||||
|
||||
const pipelineId = useStore(s => s.pipelineId)
|
||||
const invalidAllLastRun = useInvalidAllLastRun(FlowType.ragPipeline, pipelineId)
|
||||
const { fetchInspectVars } = useSetWorkflowVarsWithValue({
|
||||
flowType: FlowType.ragPipeline,
|
||||
flowId: pipelineId!,
|
||||
})
|
||||
|
||||
const handleRun = useCallback(async (
|
||||
params: any,
|
||||
callback?: IOtherOptions,
|
||||
) => {
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
} = store.getState()
|
||||
const newNodes = produce(getNodes(), (draft) => {
|
||||
draft.forEach((node) => {
|
||||
node.data.selected = false
|
||||
node.data._runningStatus = undefined
|
||||
})
|
||||
})
|
||||
setNodes(newNodes)
|
||||
await doSyncWorkflowDraft()
|
||||
|
||||
const {
|
||||
onWorkflowStarted,
|
||||
onWorkflowFinished,
|
||||
onNodeStarted,
|
||||
onNodeFinished,
|
||||
onIterationStart,
|
||||
onIterationNext,
|
||||
onIterationFinish,
|
||||
onLoopStart,
|
||||
onLoopNext,
|
||||
onLoopFinish,
|
||||
onNodeRetry,
|
||||
onAgentLog,
|
||||
onError,
|
||||
...restCallback
|
||||
} = callback || {}
|
||||
const { pipelineId } = workflowStore.getState()
|
||||
workflowStore.setState({ historyWorkflowData: undefined })
|
||||
const workflowContainer = document.getElementById('workflow-container')
|
||||
|
||||
const {
|
||||
clientWidth,
|
||||
clientHeight,
|
||||
} = workflowContainer!
|
||||
|
||||
const url = `/rag/pipelines/${pipelineId}/workflows/draft/run`
|
||||
|
||||
const {
|
||||
setWorkflowRunningData,
|
||||
} = workflowStore.getState()
|
||||
setWorkflowRunningData({
|
||||
result: {
|
||||
inputs_truncated: false,
|
||||
process_data_truncated: false,
|
||||
outputs_truncated: false,
|
||||
status: WorkflowRunningStatus.Running,
|
||||
},
|
||||
tracing: [],
|
||||
resultText: '',
|
||||
})
|
||||
|
||||
ssePost(
|
||||
url,
|
||||
{
|
||||
body: params,
|
||||
},
|
||||
{
|
||||
onWorkflowStarted: (params) => {
|
||||
handleWorkflowStarted(params)
|
||||
|
||||
if (onWorkflowStarted)
|
||||
onWorkflowStarted(params)
|
||||
},
|
||||
onWorkflowFinished: (params) => {
|
||||
handleWorkflowFinished(params)
|
||||
fetchInspectVars({})
|
||||
invalidAllLastRun()
|
||||
|
||||
if (onWorkflowFinished)
|
||||
onWorkflowFinished(params)
|
||||
},
|
||||
onError: (params) => {
|
||||
handleWorkflowFailed()
|
||||
|
||||
if (onError)
|
||||
onError(params)
|
||||
},
|
||||
onNodeStarted: (params) => {
|
||||
handleWorkflowNodeStarted(
|
||||
params,
|
||||
{
|
||||
clientWidth,
|
||||
clientHeight,
|
||||
},
|
||||
)
|
||||
|
||||
if (onNodeStarted)
|
||||
onNodeStarted(params)
|
||||
},
|
||||
onNodeFinished: (params) => {
|
||||
handleWorkflowNodeFinished(params)
|
||||
|
||||
if (onNodeFinished)
|
||||
onNodeFinished(params)
|
||||
},
|
||||
onIterationStart: (params) => {
|
||||
handleWorkflowNodeIterationStarted(
|
||||
params,
|
||||
{
|
||||
clientWidth,
|
||||
clientHeight,
|
||||
},
|
||||
)
|
||||
|
||||
if (onIterationStart)
|
||||
onIterationStart(params)
|
||||
},
|
||||
onIterationNext: (params) => {
|
||||
handleWorkflowNodeIterationNext(params)
|
||||
|
||||
if (onIterationNext)
|
||||
onIterationNext(params)
|
||||
},
|
||||
onIterationFinish: (params) => {
|
||||
handleWorkflowNodeIterationFinished(params)
|
||||
|
||||
if (onIterationFinish)
|
||||
onIterationFinish(params)
|
||||
},
|
||||
onLoopStart: (params) => {
|
||||
handleWorkflowNodeLoopStarted(
|
||||
params,
|
||||
{
|
||||
clientWidth,
|
||||
clientHeight,
|
||||
},
|
||||
)
|
||||
|
||||
if (onLoopStart)
|
||||
onLoopStart(params)
|
||||
},
|
||||
onLoopNext: (params) => {
|
||||
handleWorkflowNodeLoopNext(params)
|
||||
|
||||
if (onLoopNext)
|
||||
onLoopNext(params)
|
||||
},
|
||||
onLoopFinish: (params) => {
|
||||
handleWorkflowNodeLoopFinished(params)
|
||||
|
||||
if (onLoopFinish)
|
||||
onLoopFinish(params)
|
||||
},
|
||||
onNodeRetry: (params) => {
|
||||
handleWorkflowNodeRetry(params)
|
||||
|
||||
if (onNodeRetry)
|
||||
onNodeRetry(params)
|
||||
},
|
||||
onAgentLog: (params) => {
|
||||
handleWorkflowAgentLog(params)
|
||||
|
||||
if (onAgentLog)
|
||||
onAgentLog(params)
|
||||
},
|
||||
onTextChunk: (params) => {
|
||||
handleWorkflowTextChunk(params)
|
||||
},
|
||||
onTextReplace: (params) => {
|
||||
handleWorkflowTextReplace(params)
|
||||
},
|
||||
...restCallback,
|
||||
},
|
||||
)
|
||||
}, [
|
||||
store,
|
||||
workflowStore,
|
||||
doSyncWorkflowDraft,
|
||||
handleWorkflowStarted,
|
||||
handleWorkflowFinished,
|
||||
handleWorkflowFailed,
|
||||
handleWorkflowNodeStarted,
|
||||
handleWorkflowNodeFinished,
|
||||
handleWorkflowNodeIterationStarted,
|
||||
handleWorkflowNodeIterationNext,
|
||||
handleWorkflowNodeIterationFinished,
|
||||
handleWorkflowNodeLoopStarted,
|
||||
handleWorkflowNodeLoopNext,
|
||||
handleWorkflowNodeLoopFinished,
|
||||
handleWorkflowNodeRetry,
|
||||
handleWorkflowTextChunk,
|
||||
handleWorkflowTextReplace,
|
||||
handleWorkflowAgentLog,
|
||||
],
|
||||
)
|
||||
|
||||
const handleStopRun = useCallback((taskId: string) => {
|
||||
const { pipelineId } = workflowStore.getState()
|
||||
|
||||
stopWorkflowRun(`/rag/pipelines/${pipelineId}/workflow-runs/tasks/${taskId}/stop`)
|
||||
}, [workflowStore])
|
||||
|
||||
const handleRestoreFromPublishedWorkflow = useCallback((publishedWorkflow: VersionHistory) => {
|
||||
const nodes = publishedWorkflow.graph.nodes.map(node => ({ ...node, selected: false, data: { ...node.data, selected: false } }))
|
||||
const edges = publishedWorkflow.graph.edges
|
||||
const viewport = publishedWorkflow.graph.viewport!
|
||||
handleUpdateWorkflowCanvas({
|
||||
nodes,
|
||||
edges,
|
||||
viewport,
|
||||
})
|
||||
|
||||
workflowStore.getState().setEnvironmentVariables(publishedWorkflow.environment_variables || [])
|
||||
workflowStore.getState().setRagPipelineVariables?.(publishedWorkflow.rag_pipeline_variables || [])
|
||||
}, [handleUpdateWorkflowCanvas, workflowStore])
|
||||
|
||||
return {
|
||||
handleBackupDraft,
|
||||
handleLoadBackupDraft,
|
||||
handleRun,
|
||||
handleStopRun,
|
||||
handleRestoreFromPublishedWorkflow,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { useCallback } from 'react'
|
||||
import { useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import {
|
||||
WorkflowRunningStatus,
|
||||
} from '@/app/components/workflow/types'
|
||||
import { useWorkflowInteractions } from '@/app/components/workflow/hooks'
|
||||
import {
|
||||
useInputFieldPanel,
|
||||
useNodesSyncDraft,
|
||||
} from '.'
|
||||
|
||||
export const usePipelineStartRun = () => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const { handleCancelDebugAndPreviewPanel } = useWorkflowInteractions()
|
||||
const { doSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
const { closeAllInputFieldPanels } = useInputFieldPanel()
|
||||
|
||||
const handleWorkflowStartRunInWorkflow = useCallback(async () => {
|
||||
const {
|
||||
workflowRunningData,
|
||||
} = workflowStore.getState()
|
||||
|
||||
if (workflowRunningData?.result.status === WorkflowRunningStatus.Running)
|
||||
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])
|
||||
|
||||
const handleStartWorkflowRun = useCallback(() => {
|
||||
handleWorkflowStartRunInWorkflow()
|
||||
}, [handleWorkflowStartRunInWorkflow])
|
||||
|
||||
return {
|
||||
handleStartWorkflowRun,
|
||||
handleWorkflowStartRunInWorkflow,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { generateNewNode } from '@/app/components/workflow/utils'
|
||||
import {
|
||||
START_INITIAL_POSITION,
|
||||
} from '@/app/components/workflow/constants'
|
||||
import type { KnowledgeBaseNodeType } from '@/app/components/workflow/nodes/knowledge-base/types'
|
||||
import knowledgeBaseDefault from '@/app/components/workflow/nodes/knowledge-base/default'
|
||||
|
||||
export const usePipelineTemplate = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { newNode: knowledgeBaseNode } = generateNewNode({
|
||||
id: 'knowledgeBase',
|
||||
data: {
|
||||
...knowledgeBaseDefault.defaultValue as KnowledgeBaseNodeType,
|
||||
type: knowledgeBaseDefault.metaData.type,
|
||||
title: t(`workflow.blocks.${knowledgeBaseDefault.metaData.type}`),
|
||||
selected: true,
|
||||
},
|
||||
position: {
|
||||
x: START_INITIAL_POSITION.x + 500,
|
||||
y: START_INITIAL_POSITION.y,
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
nodes: [knowledgeBaseNode],
|
||||
edges: [],
|
||||
}
|
||||
}
|
||||
115
dify/web/app/components/rag-pipeline/hooks/use-pipeline.tsx
Normal file
115
dify/web/app/components/rag-pipeline/hooks/use-pipeline.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import { useCallback } from 'react'
|
||||
import { getOutgoers, useStoreApi } from 'reactflow'
|
||||
import { BlockEnum, type Node, type ValueSelector } from '../../workflow/types'
|
||||
import { uniqBy } from 'lodash-es'
|
||||
import { findUsedVarNodes, updateNodeVars } from '../../workflow/nodes/_base/components/variable/utils'
|
||||
import type { DataSourceNodeType } from '../../workflow/nodes/data-source/types'
|
||||
|
||||
export const usePipeline = () => {
|
||||
const store = useStoreApi()
|
||||
|
||||
const getAllDatasourceNodes = useCallback(() => {
|
||||
const {
|
||||
getNodes,
|
||||
} = store.getState()
|
||||
const nodes = getNodes() as Node<DataSourceNodeType>[]
|
||||
const datasourceNodes = nodes.filter(node => node.data.type === BlockEnum.DataSource)
|
||||
|
||||
return datasourceNodes
|
||||
}, [store])
|
||||
|
||||
const getAllNodesInSameBranch = useCallback((nodeId: string) => {
|
||||
const {
|
||||
getNodes,
|
||||
edges,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
const list: Node[] = []
|
||||
|
||||
const traverse = (root: Node, callback: (node: Node) => void) => {
|
||||
if (root) {
|
||||
const outgoers = getOutgoers(root, nodes, edges)
|
||||
|
||||
if (outgoers.length) {
|
||||
outgoers.forEach((node) => {
|
||||
callback(node)
|
||||
traverse(node, callback)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (nodeId === 'shared') {
|
||||
const allDatasourceNodes = getAllDatasourceNodes()
|
||||
|
||||
if (allDatasourceNodes.length === 0)
|
||||
return []
|
||||
|
||||
list.push(...allDatasourceNodes)
|
||||
|
||||
allDatasourceNodes.forEach((node) => {
|
||||
traverse(node, (childNode) => {
|
||||
list.push(childNode)
|
||||
})
|
||||
})
|
||||
}
|
||||
else {
|
||||
const currentNode = nodes.find(node => node.id === nodeId)!
|
||||
|
||||
if (!currentNode)
|
||||
return []
|
||||
|
||||
list.push(currentNode)
|
||||
|
||||
traverse(currentNode, (node) => {
|
||||
list.push(node)
|
||||
})
|
||||
}
|
||||
|
||||
return uniqBy(list, 'id')
|
||||
}, [getAllDatasourceNodes, store])
|
||||
|
||||
const isVarUsedInNodes = useCallback((varSelector: ValueSelector) => {
|
||||
const nodeId = varSelector[1] // Assuming the first element is always 'VARIABLE_PREFIX'(rag)
|
||||
const afterNodes = getAllNodesInSameBranch(nodeId)
|
||||
const effectNodes = findUsedVarNodes(varSelector, afterNodes)
|
||||
return effectNodes.length > 0
|
||||
}, [getAllNodesInSameBranch])
|
||||
|
||||
const handleInputVarRename = useCallback((nodeId: string, oldValeSelector: ValueSelector, newVarSelector: ValueSelector) => {
|
||||
const { getNodes, setNodes } = store.getState()
|
||||
const afterNodes = getAllNodesInSameBranch(nodeId)
|
||||
const effectNodes = findUsedVarNodes(oldValeSelector, afterNodes)
|
||||
if (effectNodes.length > 0) {
|
||||
const newNodes = getNodes().map((node) => {
|
||||
if (effectNodes.find(n => n.id === node.id))
|
||||
return updateNodeVars(node, oldValeSelector, newVarSelector)
|
||||
|
||||
return node
|
||||
})
|
||||
setNodes(newNodes)
|
||||
}
|
||||
}, [getAllNodesInSameBranch, store])
|
||||
|
||||
const removeUsedVarInNodes = useCallback((varSelector: ValueSelector) => {
|
||||
const nodeId = varSelector[1] // Assuming the first element is always 'VARIABLE_PREFIX'(rag)
|
||||
const { getNodes, setNodes } = store.getState()
|
||||
const afterNodes = getAllNodesInSameBranch(nodeId)
|
||||
const effectNodes = findUsedVarNodes(varSelector, afterNodes)
|
||||
if (effectNodes.length > 0) {
|
||||
const newNodes = getNodes().map((node) => {
|
||||
if (effectNodes.find(n => n.id === node.id))
|
||||
return updateNodeVars(node, varSelector, [])
|
||||
|
||||
return node
|
||||
})
|
||||
setNodes(newNodes)
|
||||
}
|
||||
}, [getAllNodesInSameBranch, store])
|
||||
|
||||
return {
|
||||
handleInputVarRename,
|
||||
isVarUsedInNodes,
|
||||
removeUsedVarInNodes,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useMemo } from 'react'
|
||||
import useNodes from '@/app/components/workflow/store/workflow/use-nodes'
|
||||
import { useNodesInteractions } from '@/app/components/workflow/hooks/use-nodes-interactions'
|
||||
import type { CommonNodeType } from '@/app/components/workflow/types'
|
||||
import { ragPipelineNodesAction } from '@/app/components/goto-anything/actions/rag-pipeline-nodes'
|
||||
import BlockIcon from '@/app/components/workflow/block-icon'
|
||||
import { setupNodeSelectionListener } from '@/app/components/workflow/utils/node-navigation'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import type { LLMNodeType } from '@/app/components/workflow/nodes/llm/types'
|
||||
import type { ToolNodeType } from '@/app/components/workflow/nodes/tool/types'
|
||||
import type { KnowledgeRetrievalNodeType } from '@/app/components/workflow/nodes/knowledge-retrieval/types'
|
||||
import { useGetToolIcon } from '@/app/components/workflow/hooks/use-tool-icon'
|
||||
|
||||
/**
|
||||
* Hook to register RAG pipeline nodes search functionality
|
||||
*/
|
||||
export const useRagPipelineSearch = () => {
|
||||
const nodes = useNodes()
|
||||
const { handleNodeSelect } = useNodesInteractions()
|
||||
const getToolIcon = useGetToolIcon()
|
||||
|
||||
// Process nodes to create searchable data structure
|
||||
const searchableNodes = useMemo(() => {
|
||||
return nodes.map((node) => {
|
||||
const nodeData = node.data as CommonNodeType
|
||||
const title = nodeData.title || nodeData.type || 'Untitled Node'
|
||||
let desc = nodeData.desc || ''
|
||||
|
||||
// Keep the original node title for consistency with workflow display
|
||||
// Only enhance description for better search context
|
||||
if (nodeData.type === BlockEnum.Tool) {
|
||||
const toolData = nodeData as ToolNodeType
|
||||
desc = toolData.tool_description || toolData.tool_label || desc
|
||||
}
|
||||
|
||||
if (nodeData.type === BlockEnum.LLM) {
|
||||
const llmData = nodeData as LLMNodeType
|
||||
if (llmData.model?.provider && llmData.model?.name)
|
||||
desc = `${llmData.model.name} (${llmData.model.provider}) - ${llmData.model.mode || desc}`
|
||||
}
|
||||
|
||||
if (nodeData.type === BlockEnum.KnowledgeRetrieval) {
|
||||
const knowledgeData = nodeData as KnowledgeRetrievalNodeType
|
||||
if (knowledgeData.dataset_ids?.length)
|
||||
desc = `Knowledge Retrieval with ${knowledgeData.dataset_ids.length} datasets - ${desc}`
|
||||
}
|
||||
|
||||
return {
|
||||
id: node.id,
|
||||
title,
|
||||
desc,
|
||||
type: nodeData.type,
|
||||
blockType: nodeData.type,
|
||||
nodeData,
|
||||
toolIcon: getToolIcon(nodeData),
|
||||
modelInfo: nodeData.type === BlockEnum.LLM ? {
|
||||
provider: (nodeData as LLMNodeType).model?.provider,
|
||||
name: (nodeData as LLMNodeType).model?.name,
|
||||
mode: (nodeData as LLMNodeType).model?.mode,
|
||||
} : {
|
||||
provider: undefined,
|
||||
name: undefined,
|
||||
mode: undefined,
|
||||
},
|
||||
}
|
||||
})
|
||||
}, [nodes, getToolIcon])
|
||||
|
||||
// Calculate relevance score for search results
|
||||
const calculateScore = useCallback((node: {
|
||||
title: string;
|
||||
type: string;
|
||||
desc: string;
|
||||
modelInfo: { provider?: string; name?: string; mode?: string }
|
||||
}, searchTerm: string): number => {
|
||||
if (!searchTerm) return 1
|
||||
|
||||
let score = 0
|
||||
const term = searchTerm.toLowerCase()
|
||||
|
||||
// Title match (highest priority)
|
||||
if (node.title.toLowerCase().includes(term))
|
||||
score += 10
|
||||
|
||||
// Type match
|
||||
if (node.type.toLowerCase().includes(term))
|
||||
score += 8
|
||||
|
||||
// Description match
|
||||
if (node.desc.toLowerCase().includes(term))
|
||||
score += 5
|
||||
|
||||
// Model info matches (for LLM nodes)
|
||||
if (node.modelInfo.provider?.toLowerCase().includes(term))
|
||||
score += 6
|
||||
if (node.modelInfo.name?.toLowerCase().includes(term))
|
||||
score += 6
|
||||
if (node.modelInfo.mode?.toLowerCase().includes(term))
|
||||
score += 4
|
||||
|
||||
return score
|
||||
}, [])
|
||||
|
||||
// Create search function for RAG pipeline nodes
|
||||
const searchRagPipelineNodes = useCallback((query: string) => {
|
||||
if (!searchableNodes.length) return []
|
||||
|
||||
const searchTerm = query.toLowerCase().trim()
|
||||
|
||||
const results = searchableNodes
|
||||
.map((node) => {
|
||||
const score = calculateScore(node, searchTerm)
|
||||
|
||||
return score > 0 ? {
|
||||
id: node.id,
|
||||
title: node.title,
|
||||
description: node.desc || node.type,
|
||||
type: 'workflow-node' as const,
|
||||
path: `#${node.id}`,
|
||||
icon: (
|
||||
<BlockIcon
|
||||
type={node.blockType}
|
||||
className="shrink-0"
|
||||
size="sm"
|
||||
toolIcon={node.toolIcon}
|
||||
/>
|
||||
),
|
||||
metadata: {
|
||||
nodeId: node.id,
|
||||
nodeData: node.nodeData,
|
||||
},
|
||||
data: node.nodeData,
|
||||
score,
|
||||
} : null
|
||||
})
|
||||
.filter((node): node is NonNullable<typeof node> => node !== null)
|
||||
.sort((a, b) => {
|
||||
// If no search term, sort alphabetically
|
||||
if (!searchTerm) return a.title.localeCompare(b.title)
|
||||
// Sort by relevance score (higher score first)
|
||||
return (b.score || 0) - (a.score || 0)
|
||||
})
|
||||
|
||||
return results
|
||||
}, [searchableNodes, calculateScore])
|
||||
|
||||
// Directly set the search function on the action object
|
||||
useEffect(() => {
|
||||
if (searchableNodes.length > 0) {
|
||||
// Set the search function directly on the action
|
||||
ragPipelineNodesAction.searchFn = searchRagPipelineNodes
|
||||
}
|
||||
|
||||
return () => {
|
||||
// Clean up when component unmounts
|
||||
ragPipelineNodesAction.searchFn = undefined
|
||||
}
|
||||
}, [searchableNodes, searchRagPipelineNodes])
|
||||
|
||||
// Set up node selection event listener using the utility function
|
||||
useEffect(() => {
|
||||
return setupNodeSelectionListener(handleNodeSelect)
|
||||
}, [handleNodeSelect])
|
||||
|
||||
return null
|
||||
}
|
||||
78
dify/web/app/components/rag-pipeline/index.tsx
Normal file
78
dify/web/app/components/rag-pipeline/index.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import { useMemo } from 'react'
|
||||
import WorkflowWithDefaultContext from '@/app/components/workflow'
|
||||
import {
|
||||
WorkflowContextProvider,
|
||||
} from '@/app/components/workflow/context'
|
||||
import type { InjectWorkflowStoreSliceFn } from '@/app/components/workflow/store'
|
||||
import {
|
||||
initialEdges,
|
||||
initialNodes,
|
||||
} from '@/app/components/workflow/utils'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { createRagPipelineSliceSlice } from './store'
|
||||
import RagPipelineMain from './components/rag-pipeline-main'
|
||||
import { usePipelineInit } from './hooks'
|
||||
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
|
||||
import Conversion from './components/conversion'
|
||||
import { processNodesWithoutDataSource } from './utils'
|
||||
|
||||
const RagPipeline = () => {
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
} = usePipelineInit()
|
||||
const nodesData = useMemo(() => {
|
||||
if (data)
|
||||
return initialNodes(data.graph.nodes, data.graph.edges)
|
||||
|
||||
return []
|
||||
}, [data])
|
||||
const edgesData = useMemo(() => {
|
||||
if (data)
|
||||
return initialEdges(data.graph.edges, data.graph.nodes)
|
||||
|
||||
return []
|
||||
}, [data])
|
||||
|
||||
if (!data || isLoading) {
|
||||
return (
|
||||
<div className='relative flex h-full w-full items-center justify-center'>
|
||||
<Loading />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const {
|
||||
nodes: processedNodes,
|
||||
viewport,
|
||||
} = processNodesWithoutDataSource(nodesData, data.graph.viewport)
|
||||
return (
|
||||
<WorkflowWithDefaultContext
|
||||
edges={edgesData}
|
||||
nodes={processedNodes}
|
||||
>
|
||||
<RagPipelineMain
|
||||
edges={edgesData}
|
||||
nodes={processedNodes}
|
||||
viewport={viewport}
|
||||
/>
|
||||
</WorkflowWithDefaultContext>
|
||||
)
|
||||
}
|
||||
|
||||
const RagPipelineWrapper = () => {
|
||||
const pipelineId = useDatasetDetailContextWithSelector(s => s.dataset?.pipeline_id)
|
||||
|
||||
if (!pipelineId)
|
||||
return <Conversion />
|
||||
|
||||
return (
|
||||
<WorkflowContextProvider
|
||||
injectWorkflowStoreSliceFn={createRagPipelineSliceSlice as InjectWorkflowStoreSliceFn}
|
||||
>
|
||||
<RagPipeline />
|
||||
</WorkflowContextProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default RagPipelineWrapper
|
||||
52
dify/web/app/components/rag-pipeline/store/index.ts
Normal file
52
dify/web/app/components/rag-pipeline/store/index.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import type { RAGPipelineVariables } from '@/models/pipeline'
|
||||
import type { StateCreator } from 'zustand'
|
||||
import type {
|
||||
ToolWithProvider,
|
||||
} from '@/app/components/workflow/types'
|
||||
import type { DataSourceItem } from '@/app/components/workflow/block-selector/types'
|
||||
import { transformDataSourceToTool } from '@/app/components/workflow/block-selector/utils'
|
||||
import type { IconInfo } from '@/models/datasets'
|
||||
import type { InputFieldEditorProps } from '../components/panel/input-field/editor'
|
||||
|
||||
export type RagPipelineSliceShape = {
|
||||
pipelineId: string
|
||||
knowledgeName: string
|
||||
knowledgeIcon?: IconInfo
|
||||
showInputFieldPanel: boolean
|
||||
setShowInputFieldPanel: (showInputFieldPanel: boolean) => void
|
||||
showInputFieldPreviewPanel: boolean
|
||||
setShowInputFieldPreviewPanel: (showInputFieldPreviewPanel: boolean) => void
|
||||
inputFieldEditPanelProps: InputFieldEditorProps | null
|
||||
setInputFieldEditPanelProps: (showInputFieldEditPanel: InputFieldEditorProps | null) => void
|
||||
nodesDefaultConfigs: Record<string, any>
|
||||
setNodesDefaultConfigs: (nodesDefaultConfigs: Record<string, any>) => void
|
||||
ragPipelineVariables: RAGPipelineVariables
|
||||
setRagPipelineVariables: (ragPipelineVariables: RAGPipelineVariables) => void
|
||||
dataSourceList: ToolWithProvider[]
|
||||
setDataSourceList: (dataSourceList: DataSourceItem[]) => void
|
||||
isPreparingDataSource: boolean
|
||||
setIsPreparingDataSource: (isPreparingDataSource: boolean) => void
|
||||
}
|
||||
|
||||
export type CreateRagPipelineSliceSlice = StateCreator<RagPipelineSliceShape>
|
||||
export const createRagPipelineSliceSlice: StateCreator<RagPipelineSliceShape> = set => ({
|
||||
pipelineId: '',
|
||||
knowledgeName: '',
|
||||
showInputFieldPanel: false,
|
||||
setShowInputFieldPanel: showInputFieldPanel => set(() => ({ showInputFieldPanel })),
|
||||
showInputFieldPreviewPanel: false,
|
||||
setShowInputFieldPreviewPanel: showInputFieldPreviewPanel => set(() => ({ showInputFieldPreviewPanel })),
|
||||
inputFieldEditPanelProps: null,
|
||||
setInputFieldEditPanelProps: inputFieldEditPanelProps => set(() => ({ inputFieldEditPanelProps })),
|
||||
nodesDefaultConfigs: {},
|
||||
setNodesDefaultConfigs: nodesDefaultConfigs => set(() => ({ nodesDefaultConfigs })),
|
||||
ragPipelineVariables: [],
|
||||
setRagPipelineVariables: (ragPipelineVariables: RAGPipelineVariables) => set(() => ({ ragPipelineVariables })),
|
||||
dataSourceList: [],
|
||||
setDataSourceList: (dataSourceList: DataSourceItem[]) => {
|
||||
const formattedDataSourceList = dataSourceList.map(item => transformDataSourceToTool(item))
|
||||
set(() => ({ dataSourceList: formattedDataSourceList }))
|
||||
},
|
||||
isPreparingDataSource: false,
|
||||
setIsPreparingDataSource: isPreparingDataSource => set(() => ({ isPreparingDataSource })),
|
||||
})
|
||||
1
dify/web/app/components/rag-pipeline/utils/index.ts
Normal file
1
dify/web/app/components/rag-pipeline/utils/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './nodes'
|
||||
90
dify/web/app/components/rag-pipeline/utils/nodes.ts
Normal file
90
dify/web/app/components/rag-pipeline/utils/nodes.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import type { Viewport } from 'reactflow'
|
||||
import type { Node } from '@/app/components/workflow/types'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { generateNewNode } from '@/app/components/workflow/utils'
|
||||
import { CUSTOM_DATA_SOURCE_EMPTY_NODE } from '@/app/components/workflow/nodes/data-source-empty/constants'
|
||||
import { CUSTOM_NOTE_NODE } from '@/app/components/workflow/note-node/constants'
|
||||
import { NoteTheme } from '@/app/components/workflow/note-node/types'
|
||||
import type { NoteNodeType } from '@/app/components/workflow/note-node/types'
|
||||
import {
|
||||
CUSTOM_NODE,
|
||||
NODE_WIDTH_X_OFFSET,
|
||||
START_INITIAL_POSITION,
|
||||
} from '@/app/components/workflow/constants'
|
||||
|
||||
export const processNodesWithoutDataSource = (nodes: Node[], viewport?: Viewport) => {
|
||||
let leftNode
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const node = nodes[i]
|
||||
|
||||
if (node.data.type === BlockEnum.DataSource) {
|
||||
return {
|
||||
nodes,
|
||||
viewport,
|
||||
}
|
||||
}
|
||||
|
||||
if (node.type === CUSTOM_NODE && !leftNode)
|
||||
leftNode = node
|
||||
|
||||
if (node.type === CUSTOM_NODE && leftNode && node.position.x < leftNode.position.x)
|
||||
leftNode = node
|
||||
}
|
||||
|
||||
if (leftNode) {
|
||||
const startX = leftNode.position.x - NODE_WIDTH_X_OFFSET
|
||||
const startY = leftNode.position.y
|
||||
const { newNode } = generateNewNode({
|
||||
id: 'data-source-empty',
|
||||
type: CUSTOM_DATA_SOURCE_EMPTY_NODE,
|
||||
data: {
|
||||
title: '',
|
||||
desc: '',
|
||||
type: BlockEnum.DataSourceEmpty,
|
||||
width: 240,
|
||||
_isTempNode: true,
|
||||
},
|
||||
position: {
|
||||
x: startX,
|
||||
y: startY,
|
||||
},
|
||||
})
|
||||
const newNoteNode = generateNewNode({
|
||||
id: 'note',
|
||||
type: CUSTOM_NOTE_NODE,
|
||||
data: {
|
||||
title: '',
|
||||
desc: '',
|
||||
type: '' as any,
|
||||
text: '{"root":{"children":[{"children":[{"detail":0,"format":1,"mode":"normal","style":"font-size: 14px;","text":"Get started with a blank pipeline","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"paragraph","version":1,"textFormat":1,"textStyle":"font-size: 14px;"},{"children":[],"direction":"ltr","format":"","indent":0,"type":"paragraph","version":1,"textFormat":1,"textStyle":""},{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"A Knowledge Pipeline starts with Data Source as the starting node and ends with the knowledge base node. The general steps are: import documents from the data source → use extractor to extract document content → split and clean content into structured chunks → store in the knowledge base.","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"paragraph","version":1,"textFormat":0,"textStyle":""},{"children":[],"direction":"ltr","format":"","indent":0,"type":"paragraph","version":1,"textFormat":0,"textStyle":""},{"children":[{"children":[{"detail":0,"format":2,"mode":"normal","style":"","text":"Link to documentation","type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"link","version":1,"textFormat":2,"rel":"noreferrer","target":"_blank","title":null,"url":"https://dify.ai"}],"direction":"ltr","format":"","indent":0,"type":"paragraph","version":1,"textFormat":2,"textStyle":""},{"children":[],"direction":"ltr","format":"","indent":0,"type":"paragraph","version":1,"textFormat":0,"textStyle":""}],"direction":"ltr","format":"","indent":0,"type":"root","version":1,"textFormat":1,"textStyle":"font-size: 14px;"}}',
|
||||
theme: NoteTheme.blue,
|
||||
author: '',
|
||||
showAuthor: true,
|
||||
width: 240,
|
||||
height: 300,
|
||||
_isTempNode: true,
|
||||
} as NoteNodeType,
|
||||
position: {
|
||||
x: startX,
|
||||
y: startY + 100,
|
||||
},
|
||||
}).newNode
|
||||
return {
|
||||
nodes: [
|
||||
newNode,
|
||||
newNoteNode,
|
||||
...nodes,
|
||||
],
|
||||
viewport: {
|
||||
x: (START_INITIAL_POSITION.x - startX) * (viewport?.zoom || 1),
|
||||
y: (START_INITIAL_POSITION.y - startY) * (viewport?.zoom || 1),
|
||||
zoom: viewport?.zoom || 1,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
nodes,
|
||||
viewport,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user