mirror of https://github.com/langgenius/dify.git
add datasource empty node
This commit is contained in:
parent
3b8d96f45c
commit
d76e37b018
|
|
@ -1,13 +1,13 @@
|
|||
import React, { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '../base/button'
|
||||
import Button from '@/app/components/base/button'
|
||||
import PipelineScreenShot from './screenshot'
|
||||
import Confirm from '../base/confirm'
|
||||
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 '../base/toast'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
|
||||
const Conversion = () => {
|
||||
const { t } = useTranslation()
|
||||
|
|
@ -3,6 +3,7 @@ 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'
|
||||
|
||||
|
|
@ -32,6 +33,13 @@ export const useAvailableNodesMetaData = () => {
|
|||
isUndeletable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
...dataSourceEmptyDefault,
|
||||
metaData: {
|
||||
...dataSourceEmptyDefault.metaData,
|
||||
isUndeletable: true,
|
||||
},
|
||||
},
|
||||
], [])
|
||||
|
||||
const prefixLink = useMemo(() => {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
import { API_PREFIX } from '@/config'
|
||||
import { syncWorkflowDraft } from '@/service/workflow'
|
||||
import { usePipelineRefreshDraft } from '.'
|
||||
import { CUSTOM_DATA_SOURCE_EMPTY_NODE } from '@/app/components/workflow/nodes/data-source-empty/constants'
|
||||
|
||||
export const useNodesSyncDraft = () => {
|
||||
const store = useStoreApi()
|
||||
|
|
@ -23,7 +24,8 @@ export const useNodesSyncDraft = () => {
|
|||
edges,
|
||||
transform,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
const nodesOriginal = getNodes()
|
||||
const nodes = nodesOriginal.filter(node => node.type !== CUSTOM_DATA_SOURCE_EMPTY_NODE)
|
||||
const [x, y, zoom] = transform
|
||||
const {
|
||||
pipelineId,
|
||||
|
|
|
|||
|
|
@ -13,7 +13,9 @@ import { createRagPipelineSliceSlice } from './store'
|
|||
import RagPipelineMain from './components/rag-pipeline-main'
|
||||
import { usePipelineInit } from './hooks'
|
||||
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
|
||||
import Conversion from './conversion'
|
||||
import Conversion from './components/conversion'
|
||||
import type { Node } from '@/app/components/workflow/types'
|
||||
import { processNodesWithoutDataSource } from './utils'
|
||||
|
||||
const RagPipeline = () => {
|
||||
const {
|
||||
|
|
@ -21,10 +23,11 @@ const RagPipeline = () => {
|
|||
isLoading,
|
||||
} = usePipelineInit()
|
||||
const nodesData = useMemo(() => {
|
||||
let result: Node[] = []
|
||||
if (data)
|
||||
return initialNodes(data.graph.nodes, data.graph.edges)
|
||||
result = initialNodes(data.graph.nodes, data.graph.edges)
|
||||
|
||||
return []
|
||||
return processNodesWithoutDataSource(result)
|
||||
}, [data])
|
||||
const edgesData = useMemo(() => {
|
||||
if (data)
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
export * from './nodes'
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
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 } from '@/app/components/workflow/constants'
|
||||
|
||||
export const processNodesWithoutDataSource = (nodes: Node[]) => {
|
||||
if (!nodes || nodes.length === 0) return []
|
||||
|
||||
let leftNode
|
||||
let hasNoteBySystem
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const node = nodes[i]
|
||||
if (node.type === CUSTOM_NOTE_NODE && node.data.noteBySystem)
|
||||
hasNoteBySystem = true
|
||||
|
||||
if (node.type !== CUSTOM_NODE)
|
||||
continue
|
||||
|
||||
if (node.data.type === BlockEnum.DataSource)
|
||||
return nodes
|
||||
|
||||
if (!leftNode)
|
||||
leftNode = node
|
||||
|
||||
if (node.position.x < leftNode.position.x)
|
||||
leftNode = node
|
||||
}
|
||||
|
||||
if (leftNode) {
|
||||
const { newNode } = generateNewNode({
|
||||
type: CUSTOM_DATA_SOURCE_EMPTY_NODE,
|
||||
data: {
|
||||
title: '',
|
||||
desc: '',
|
||||
type: BlockEnum.DataSourceEmpty,
|
||||
width: 240,
|
||||
},
|
||||
position: {
|
||||
x: leftNode.position.x - 500,
|
||||
y: leftNode.position.y,
|
||||
},
|
||||
})
|
||||
let newNoteNode
|
||||
if (!hasNoteBySystem) {
|
||||
newNoteNode = generateNewNode({
|
||||
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":null,"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,
|
||||
noteBySystem: true,
|
||||
} as NoteNodeType,
|
||||
position: {
|
||||
x: leftNode.position.x - 500,
|
||||
y: leftNode.position.y + 100,
|
||||
},
|
||||
}).newNode
|
||||
}
|
||||
return [
|
||||
newNode,
|
||||
...(newNoteNode ? [newNoteNode] : []),
|
||||
...nodes,
|
||||
]
|
||||
}
|
||||
|
||||
return nodes
|
||||
}
|
||||
|
|
@ -2,6 +2,9 @@ import {
|
|||
useCallback,
|
||||
useRef,
|
||||
} from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiArrowRightUpLine } from '@remixicon/react'
|
||||
import { BlockEnum } from '../types'
|
||||
import type {
|
||||
OnSelectBlock,
|
||||
|
|
@ -12,6 +15,8 @@ import Tools from './tools'
|
|||
import { ViewType } from './view-type-select'
|
||||
import cn from '@/utils/classnames'
|
||||
import type { ListRef } from '@/app/components/workflow/block-selector/market-place-plugin/list'
|
||||
import { getMarketplaceUrl } from '@/utils/var'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
|
||||
type AllToolsProps = {
|
||||
className?: string
|
||||
|
|
@ -28,6 +33,7 @@ const DataSources = ({
|
|||
onSelect,
|
||||
dataSources,
|
||||
}: AllToolsProps) => {
|
||||
const { t } = useTranslation()
|
||||
const pluginRef = useRef<ListRef>(null)
|
||||
const wrapElemRef = useRef<HTMLDivElement>(null)
|
||||
const handleSelect = useCallback((_: any, toolDefaultValue: ToolDefaultValue) => {
|
||||
|
|
@ -40,6 +46,7 @@ const DataSources = ({
|
|||
title: toolDefaultValue?.title,
|
||||
})
|
||||
}, [onSelect])
|
||||
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
|
||||
|
||||
return (
|
||||
<div className={cn(className)}>
|
||||
|
|
@ -56,6 +63,18 @@ const DataSources = ({
|
|||
hasSearchText={!!searchText}
|
||||
canNotSelectMultiple
|
||||
/>
|
||||
{
|
||||
enable_marketplace && (
|
||||
<Link
|
||||
className='system-sm-medium sticky bottom-0 z-10 flex h-8 cursor-pointer items-center rounded-b-lg border-[0.5px] border-t border-components-panel-border bg-components-panel-bg-blur px-4 py-1 text-text-accent-light-mode-only shadow-lg'
|
||||
href={getMarketplaceUrl('')}
|
||||
target='_blank'
|
||||
>
|
||||
<span>{t('plugin.findMoreInMarketplace')}</span>
|
||||
<RiArrowRightUpLine className='ml-0.5 h-3 w-3' />
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import {
|
|||
ToolTypeEnum,
|
||||
} from './types'
|
||||
|
||||
export const useTabs = (noBlocks?: boolean, noSources?: boolean) => {
|
||||
export const useTabs = (noBlocks?: boolean, noSources?: boolean, noTools?: boolean) => {
|
||||
const { t } = useTranslation()
|
||||
const tabs = useMemo(() => {
|
||||
return [
|
||||
|
|
@ -32,18 +32,27 @@ export const useTabs = (noBlocks?: boolean, noSources?: boolean) => {
|
|||
},
|
||||
]
|
||||
),
|
||||
{
|
||||
key: TabsEnum.Tools,
|
||||
name: t('workflow.tabs.tools'),
|
||||
},
|
||||
...(
|
||||
noTools
|
||||
? []
|
||||
: [
|
||||
{
|
||||
key: TabsEnum.Tools,
|
||||
name: t('workflow.tabs.tools'),
|
||||
},
|
||||
]
|
||||
),
|
||||
]
|
||||
}, [t, noBlocks, noSources])
|
||||
}, [t, noBlocks, noSources, noTools])
|
||||
const initialTab = useMemo(() => {
|
||||
if (noBlocks)
|
||||
return noSources ? TabsEnum.Tools : TabsEnum.Sources
|
||||
return noTools ? TabsEnum.Sources : TabsEnum.Tools
|
||||
|
||||
if (noTools)
|
||||
return noBlocks ? TabsEnum.Sources : TabsEnum.Blocks
|
||||
|
||||
return TabsEnum.Blocks
|
||||
}, [noBlocks, noSources])
|
||||
}, [noBlocks, noSources, noTools])
|
||||
const [activeTab, setActiveTab] = useState(initialTab)
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -30,6 +30,9 @@ const NodeSelectorWrapper = (props: NodeSelectorProps) => {
|
|||
if (block.metaData.type === BlockEnum.LoopStart)
|
||||
return false
|
||||
|
||||
if (block.metaData.type === BlockEnum.DataSourceEmpty)
|
||||
return false
|
||||
|
||||
return true
|
||||
})
|
||||
}, [availableNodesMetaData?.nodes])
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ export type NodeSelectorProps = {
|
|||
blocks?: NodeDefault[]
|
||||
dataSources?: ToolWithProvider[]
|
||||
noBlocks?: boolean
|
||||
noTools?: boolean
|
||||
}
|
||||
const NodeSelector: FC<NodeSelectorProps> = ({
|
||||
open: openFromProps,
|
||||
|
|
@ -68,6 +69,7 @@ const NodeSelector: FC<NodeSelectorProps> = ({
|
|||
blocks = [],
|
||||
dataSources = [],
|
||||
noBlocks = false,
|
||||
noTools = false,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [searchText, setSearchText] = useState('')
|
||||
|
|
@ -98,7 +100,7 @@ const NodeSelector: FC<NodeSelectorProps> = ({
|
|||
activeTab,
|
||||
setActiveTab,
|
||||
tabs,
|
||||
} = useTabs(!blocks.length, !dataSources.length)
|
||||
} = useTabs(noBlocks, !dataSources.length, noTools)
|
||||
|
||||
const handleActiveTabChange = useCallback((newActiveTab: TabsEnum) => {
|
||||
setActiveTab(newActiveTab)
|
||||
|
|
@ -165,6 +167,17 @@ const NodeSelector: FC<NodeSelectorProps> = ({
|
|||
onClear={() => setSearchText('')}
|
||||
/>
|
||||
)}
|
||||
{activeTab === TabsEnum.Sources && (
|
||||
<Input
|
||||
showLeftIcon
|
||||
showClearIcon
|
||||
autoFocus
|
||||
value={searchText}
|
||||
placeholder={searchPlaceholder}
|
||||
onChange={e => setSearchText(e.target.value)}
|
||||
onClear={() => setSearchText('')}
|
||||
/>
|
||||
)}
|
||||
{activeTab === TabsEnum.Tools && (
|
||||
<SearchBox
|
||||
search={searchText}
|
||||
|
|
@ -184,6 +197,7 @@ const NodeSelector: FC<NodeSelectorProps> = ({
|
|||
availableBlocksTypes={availableBlocksTypes}
|
||||
noBlocks={noBlocks}
|
||||
dataSources={dataSources}
|
||||
noTools={noTools}
|
||||
/>
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ export type TabsProps = {
|
|||
}>
|
||||
filterElem: React.ReactNode
|
||||
noBlocks?: boolean
|
||||
noTools?: boolean
|
||||
}
|
||||
const Tabs: FC<TabsProps> = ({
|
||||
activeTab,
|
||||
|
|
@ -41,6 +42,7 @@ const Tabs: FC<TabsProps> = ({
|
|||
tabs = [],
|
||||
filterElem,
|
||||
noBlocks,
|
||||
noTools,
|
||||
}) => {
|
||||
const { data: buildInTools } = useAllBuiltInTools()
|
||||
const { data: customTools } = useAllCustomTools()
|
||||
|
|
@ -96,7 +98,7 @@ const Tabs: FC<TabsProps> = ({
|
|||
)
|
||||
}
|
||||
{
|
||||
activeTab === TabsEnum.Tools && (
|
||||
activeTab === TabsEnum.Tools && !noTools && (
|
||||
<AllTools
|
||||
searchText={searchText}
|
||||
onSelect={onSelect}
|
||||
|
|
|
|||
|
|
@ -344,6 +344,8 @@ export const useNodesInteractions = () => {
|
|||
return
|
||||
if (node.type === CUSTOM_LOOP_START_NODE)
|
||||
return
|
||||
if (node.data.type === BlockEnum.DataSourceEmpty)
|
||||
return
|
||||
handleNodeSelect(node.id)
|
||||
}, [handleNodeSelect])
|
||||
|
||||
|
|
@ -1265,13 +1267,13 @@ export const useNodesInteractions = () => {
|
|||
if (nodeId) {
|
||||
// If nodeId is provided, copy that specific node
|
||||
const nodeToCopy = nodes.find(node => node.id === nodeId && node.data.type !== BlockEnum.Start
|
||||
&& node.type !== CUSTOM_ITERATION_START_NODE && node.type !== CUSTOM_LOOP_START_NODE && node.data.type !== BlockEnum.LoopEnd && node.data.type !== BlockEnum.KnowledgeBase)
|
||||
&& node.type !== CUSTOM_ITERATION_START_NODE && node.type !== CUSTOM_LOOP_START_NODE && node.data.type !== BlockEnum.LoopEnd && node.data.type !== BlockEnum.KnowledgeBase && node.data.type !== BlockEnum.DataSourceEmpty)
|
||||
if (nodeToCopy)
|
||||
setClipboardElements([nodeToCopy])
|
||||
}
|
||||
else {
|
||||
// If no nodeId is provided, fall back to the current behavior
|
||||
const bundledNodes = nodes.filter(node => node.data._isBundled && node.data.type !== BlockEnum.Start && node.data.type !== BlockEnum.DataSource && node.data.type !== BlockEnum.KnowledgeBase
|
||||
const bundledNodes = nodes.filter(node => node.data._isBundled && node.data.type !== BlockEnum.Start && node.data.type !== BlockEnum.DataSource && node.data.type !== BlockEnum.KnowledgeBase && node.data.type !== BlockEnum.DataSourceEmpty
|
||||
&& !node.data.isInIteration && !node.data.isInLoop)
|
||||
|
||||
if (bundledNodes.length) {
|
||||
|
|
|
|||
|
|
@ -57,6 +57,8 @@ import CustomLoopStartNode from './nodes/loop-start'
|
|||
import { CUSTOM_LOOP_START_NODE } from './nodes/loop-start/constants'
|
||||
import CustomSimpleNode from './simple-node'
|
||||
import { CUSTOM_SIMPLE_NODE } from './simple-node/constants'
|
||||
import CustomDataSourceEmptyNode from './nodes/data-source-empty'
|
||||
import { CUSTOM_DATA_SOURCE_EMPTY_NODE } from './nodes/data-source-empty/constants'
|
||||
import Operator from './operator'
|
||||
import Control from './operator/control'
|
||||
import CustomEdge from './custom-edge'
|
||||
|
|
@ -94,6 +96,7 @@ const nodeTypes = {
|
|||
[CUSTOM_SIMPLE_NODE]: CustomSimpleNode,
|
||||
[CUSTOM_ITERATION_START_NODE]: CustomIterationStartNode,
|
||||
[CUSTOM_LOOP_START_NODE]: CustomLoopStartNode,
|
||||
[CUSTOM_DATA_SOURCE_EMPTY_NODE]: CustomDataSourceEmptyNode,
|
||||
}
|
||||
const edgeTypes = {
|
||||
[CUSTOM_EDGE]: CustomEdge,
|
||||
|
|
@ -190,7 +193,6 @@ export const Workflow: FC<WorkflowProps> = memo(({
|
|||
return () => {
|
||||
handleSyncWorkflowDraft(true, true)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
const { handleRefreshWorkflowDraft } = useWorkflowRefreshDraft()
|
||||
|
|
@ -282,7 +284,6 @@ export const Workflow: FC<WorkflowProps> = memo(({
|
|||
const { fetchInspectVars } = useSetWorkflowVarsWithValue()
|
||||
useEffect(() => {
|
||||
fetchInspectVars()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
const store = useStoreApi()
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
export const CUSTOM_DATA_SOURCE_EMPTY_NODE = 'custom-data-source-empty'
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
import type { NodeDefault } from '../../types'
|
||||
import type { DataSourceEmptyNodeType } from './types'
|
||||
import { genNodeMetaData } from '@/app/components/workflow/utils'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
|
||||
const metaData = genNodeMetaData({
|
||||
sort: -1,
|
||||
type: BlockEnum.DataSourceEmpty,
|
||||
})
|
||||
const nodeDefault: NodeDefault<DataSourceEmptyNodeType> = {
|
||||
metaData,
|
||||
defaultValue: {},
|
||||
checkValid() {
|
||||
return {
|
||||
isValid: true,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default nodeDefault
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
import { useCallback } from 'react'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import { produce } from 'immer'
|
||||
import type { OnSelectBlock } from '@/app/components/workflow/types'
|
||||
import { generateNewNode } from '@/app/components/workflow/utils'
|
||||
import { useNodesMetaData } from '@/app/components/workflow/hooks'
|
||||
|
||||
export const useReplaceDataSourceNode = (id: string) => {
|
||||
const store = useStoreApi()
|
||||
const { nodesMap: nodesMetaDataMap } = useNodesMetaData()
|
||||
|
||||
const handleReplaceNode = useCallback<OnSelectBlock>((
|
||||
type,
|
||||
toolDefaultValue,
|
||||
) => {
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
const emptyNodeIndex = nodes.findIndex(node => node.id === id)
|
||||
|
||||
if (emptyNodeIndex < 0) return
|
||||
const {
|
||||
defaultValue,
|
||||
} = nodesMetaDataMap![type]
|
||||
const emptyNode = nodes[emptyNodeIndex]
|
||||
const { newNode } = generateNewNode({
|
||||
data: {
|
||||
...(defaultValue as any),
|
||||
...(toolDefaultValue || {}),
|
||||
},
|
||||
position: {
|
||||
x: emptyNode.position.x,
|
||||
y: emptyNode.position.y,
|
||||
},
|
||||
})
|
||||
const newNodes = produce(nodes, (draft) => {
|
||||
draft[emptyNodeIndex] = newNode
|
||||
})
|
||||
setNodes(newNodes)
|
||||
}, [])
|
||||
|
||||
return {
|
||||
handleReplaceNode,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { NodeProps } from 'reactflow'
|
||||
import { RiAddLine } from '@remixicon/react'
|
||||
import cn from '@/utils/classnames'
|
||||
import Button from '@/app/components/base/button'
|
||||
import BlockSelector from '@/app/components/workflow/block-selector'
|
||||
import { useReplaceDataSourceNode } from './hooks'
|
||||
|
||||
const DataSourceEmptyNode = ({ id }: NodeProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { handleReplaceNode } = useReplaceDataSourceNode(id)
|
||||
|
||||
const renderTrigger = useCallback(() => {
|
||||
return (
|
||||
<Button
|
||||
variant='primary'
|
||||
className='w-full'
|
||||
>
|
||||
<RiAddLine className='mr-1 h-4 w-4' />
|
||||
{t('workflow.nodes.dataSource.add')}
|
||||
</Button>
|
||||
)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'relative flex rounded-2xl border',
|
||||
'border-transparent',
|
||||
)}
|
||||
>
|
||||
<div className='absolute inset-[-2px] top-[-22px] z-[-1] rounded-[18px] bg-node-data-source-bg p-0.5 backdrop-blur-[6px]'>
|
||||
<div className='system-2xs-semibold-uppercase flex h-5 items-center px-2.5 text-text-tertiary'>
|
||||
{t('workflow.blocks.datasource')}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
'group relative shadow-xs',
|
||||
'rounded-[15px] border border-transparent',
|
||||
'w-[240px] bg-workflow-block-bg',
|
||||
)}
|
||||
>
|
||||
<div className={cn(
|
||||
'flex items-center rounded-t-2xl p-3',
|
||||
)}>
|
||||
<BlockSelector
|
||||
asChild
|
||||
onSelect={handleReplaceNode}
|
||||
trigger={renderTrigger}
|
||||
noBlocks
|
||||
noTools
|
||||
popupClassName='w-[320px]'
|
||||
placement='bottom-start'
|
||||
offset={{
|
||||
mainAxis: 4,
|
||||
crossAxis: 0,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(DataSourceEmptyNode)
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
import type { CommonNodeType } from '@/app/components/workflow/types'
|
||||
|
||||
export type DataSourceEmptyNodeType = CommonNodeType
|
||||
|
|
@ -47,6 +47,7 @@ export enum BlockEnum {
|
|||
LoopStart = 'loop-start',
|
||||
LoopEnd = 'loop-end',
|
||||
DataSource = 'datasource',
|
||||
DataSourceEmpty = 'datasource-empty',
|
||||
KnowledgeBase = 'knowledge-index',
|
||||
}
|
||||
|
||||
|
|
@ -84,6 +85,7 @@ export type CommonNodeType<T = {}> = {
|
|||
_waitingRun?: boolean
|
||||
_retryIndex?: number
|
||||
_dataSourceStartToAdd?: boolean
|
||||
noteBySystem?: boolean
|
||||
isInIteration?: boolean
|
||||
iteration_id?: string
|
||||
selected?: boolean
|
||||
|
|
|
|||
|
|
@ -913,6 +913,7 @@ const translation = {
|
|||
dataSource: {
|
||||
supportedFileFormats: 'Supported file formats',
|
||||
supportedFileFormatsPlaceholder: 'File extension, e.g. doc',
|
||||
add: 'Add data source',
|
||||
},
|
||||
knowledgeBase: {
|
||||
chunkStructure: 'Chunk Structure',
|
||||
|
|
|
|||
|
|
@ -927,6 +927,7 @@ const translation = {
|
|||
dataSource: {
|
||||
supportedFileFormats: '支持的文件格式',
|
||||
supportedFileFormatsPlaceholder: '文件格式,例如:doc',
|
||||
add: '添加数据源',
|
||||
},
|
||||
knowledgeBase: {
|
||||
chunkStructure: '分段结构',
|
||||
|
|
|
|||
Loading…
Reference in New Issue