block selector & data source node

This commit is contained in:
zxhlyh 2025-04-22 16:46:33 +08:00
parent efb27eb443
commit 8d9c252811
20 changed files with 201 additions and 70 deletions

View File

@ -109,7 +109,7 @@ const Blocks = ({
}, [groups, nodesExtraData, onSelect, t])
return (
<div className='p-1'>
<div className='max-h-[480px] overflow-y-auto p-1'>
{
isEmpty && (
<div className='flex h-[22px] items-center px-3 text-xs font-medium text-text-tertiary'>{t('workflow.tabs.noResult')}</div>

View File

@ -3,6 +3,12 @@ import { BlockEnum } from '../types'
import { BlockClassificationEnum } from './types'
export const BLOCKS: Block[] = [
{
classification: BlockClassificationEnum.Default,
type: BlockEnum.DataSource,
title: 'File upload',
description: '',
},
{
classification: BlockClassificationEnum.Default,
type: BlockEnum.Start,

View File

@ -1,3 +1,7 @@
import {
useMemo,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import { BLOCKS } from './constants'
import {
@ -16,19 +20,43 @@ export const useBlocks = () => {
})
}
export const useTabs = () => {
export const useTabs = (noBlocks?: boolean) => {
const { t } = useTranslation()
const tabs = useMemo(() => {
return [
...(
noBlocks
? []
: [
{
key: TabsEnum.Blocks,
name: t('workflow.tabs.blocks'),
},
]
),
{
key: TabsEnum.Sources,
name: t('workflow.tabs.sources'),
},
{
key: TabsEnum.Tools,
name: t('workflow.tabs.tools'),
},
]
}, [t, noBlocks])
const initialTab = useMemo(() => {
if (noBlocks)
return TabsEnum.Sources
return [
{
key: TabsEnum.Blocks,
name: t('workflow.tabs.blocks'),
},
{
key: TabsEnum.Tools,
name: t('workflow.tabs.tools'),
},
]
return TabsEnum.Blocks
}, [noBlocks])
const [activeTab, setActiveTab] = useState(initialTab)
return {
tabs,
activeTab,
setActiveTab,
}
}
export const useToolTabs = () => {

View File

@ -16,13 +16,15 @@ import type {
import type { BlockEnum, OnSelectBlock } from '../types'
import Tabs from './tabs'
import { TabsEnum } from './types'
import { useTabs } from './hooks'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import Input from '@/app/components/base/input'
import SearchBox from '@/app/components/plugins/marketplace/search-box'
// import SearchBox from '@/app/components/plugins/marketplace/search-box'
import cn from '@/utils/classnames'
import {
Plus02,
@ -85,10 +87,12 @@ const NodeSelector: FC<NodeSelectorProps> = ({
onSelect(type, toolDefaultValue)
}, [handleOpenChange, onSelect])
const [activeTab, setActiveTab] = useState(noBlocks ? TabsEnum.Tools : TabsEnum.Blocks)
const handleActiveTabChange = useCallback((newActiveTab: TabsEnum) => {
setActiveTab(newActiveTab)
}, [])
const {
activeTab,
setActiveTab,
tabs,
} = useTabs()
const searchPlaceholder = useMemo(() => {
if (activeTab === TabsEnum.Blocks)
return t('workflow.tabs.searchBlock')
@ -128,9 +132,31 @@ const NodeSelector: FC<NodeSelectorProps> = ({
}
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[1000]'>
<div className={`rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg ${popupClassName}`}>
<div className='px-2 pt-2' onClick={e => e.stopPropagation()}>
{activeTab === TabsEnum.Blocks && (
<div className={cn(
'overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg backdrop-blur-[5px]',
popupClassName,
)}>
<div className='border-b border-divider-subtle bg-background-section-burn'>
<div className='flex h-9 items-center px-1 pt-1'>
{
tabs.map(tab => (
<div
key={tab.key}
className={cn(
'system-sm-medium mr-0.5 cursor-pointer rounded-t-lg px-3 py-2 text-text-tertiary hover:bg-state-base-hover',
activeTab === tab.key && 'bg-components-panel-bg text-text-accent shadow-sm',
)}
onClick={(e) => {
e.stopPropagation()
setActiveTab(tab.key)
}}
>
{tab.name}
</div>
))
}
</div>
<div className='relative z-[1] bg-components-panel-bg p-2'>
<Input
showLeftIcon
showClearIcon
@ -140,6 +166,10 @@ const NodeSelector: FC<NodeSelectorProps> = ({
onChange={e => setSearchText(e.target.value)}
onClear={() => setSearchText('')}
/>
</div>
</div>
{/* <div className='p-2' onClick={e => e.stopPropagation()}>
{activeTab === TabsEnum.Blocks && (
)}
{activeTab === TabsEnum.Tools && (
<SearchBox
@ -151,11 +181,9 @@ const NodeSelector: FC<NodeSelectorProps> = ({
placeholder={t('plugin.searchTools')!}
/>
)}
</div>
</div> */}
<Tabs
activeTab={activeTab}
onActiveTabChange={handleActiveTabChange}
onSelect={handleSelect}
searchText={searchText}
tags={tags}

View File

@ -2,16 +2,13 @@ import type { FC } from 'react'
import { memo } from 'react'
import { useAllBuiltInTools, useAllCustomTools, useAllWorkflowTools } from '@/service/use-tools'
import type { BlockEnum } from '../types'
import { useTabs } from './hooks'
import type { ToolDefaultValue } from './types'
import { TabsEnum } from './types'
import Blocks from './blocks'
import AllTools from './all-tools'
import cn from '@/utils/classnames'
export type TabsProps = {
activeTab: TabsEnum
onActiveTabChange: (activeTab: TabsEnum) => void
searchText: string
tags: string[]
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
@ -20,42 +17,18 @@ export type TabsProps = {
}
const Tabs: FC<TabsProps> = ({
activeTab,
onActiveTabChange,
tags,
searchText,
onSelect,
availableBlocksTypes,
noBlocks,
}) => {
const tabs = useTabs()
const { data: buildInTools } = useAllBuiltInTools()
const { data: customTools } = useAllCustomTools()
const { data: workflowTools } = useAllWorkflowTools()
return (
<div onClick={e => e.stopPropagation()}>
{
!noBlocks && (
<div className='flex items-center border-b-[0.5px] border-divider-subtle px-3'>
{
tabs.map(tab => (
<div
key={tab.key}
className={cn(
'system-sm-medium relative mr-4 cursor-pointer pb-2 pt-1',
activeTab === tab.key
? 'text-text-primary after:absolute after:bottom-0 after:left-0 after:h-0.5 after:w-full after:bg-util-colors-blue-brand-blue-brand-600'
: 'text-text-tertiary',
)}
onClick={() => onActiveTabChange(tab.key)}
>
{tab.name}
</div>
))
}
</div>
)
}
{
activeTab === TabsEnum.Blocks && !noBlocks && (
<Blocks

View File

@ -1,6 +1,7 @@
export enum TabsEnum {
Blocks = 'blocks',
Tools = 'tools',
Sources = 'sources',
}
export enum ToolTypeEnum {

View File

@ -22,6 +22,7 @@ import IterationStartDefault from './nodes/iteration-start/default'
import AgentDefault from './nodes/agent/default'
import LoopStartDefault from './nodes/loop-start/default'
import LoopEndDefault from './nodes/loop-end/default'
import DataSourceDefault from './nodes/data-source/default'
type NodesExtraData = {
author: string
@ -33,6 +34,15 @@ type NodesExtraData = {
checkValid: any
}
export const NODES_EXTRA_DATA: Record<BlockEnum, NodesExtraData> = {
[BlockEnum.DataSource]: {
author: 'Dify',
about: '',
availablePrevNodes: [],
availableNextNodes: [],
getAvailablePrevNodes: DataSourceDefault.getAvailablePrevNodes,
getAvailableNextNodes: DataSourceDefault.getAvailableNextNodes,
checkValid: DataSourceDefault.checkValid,
},
[BlockEnum.Start]: {
author: 'Dify',
about: '',
@ -243,6 +253,12 @@ export const NODES_EXTRA_DATA: Record<BlockEnum, NodesExtraData> = {
}
export const NODES_INITIAL_DATA = {
[BlockEnum.DataSource]: {
type: BlockEnum.DataSource,
title: '',
desc: '',
...DataSourceDefault.defaultValue,
},
[BlockEnum.Start]: {
type: BlockEnum.Start,
title: '',

View File

@ -25,11 +25,8 @@ import {
useStore,
useWorkflowStore,
} from '../store'
import {
getParallelInfo,
} from '../utils'
import {
PARALLEL_DEPTH_LIMIT,
PARALLEL_LIMIT,
SUPPORT_OUTPUT_VARS_NODE,
} from '../constants'
@ -323,24 +320,24 @@ export const useWorkflow = () => {
}, [store, workflowStore, t])
const checkNestedParallelLimit = useCallback((nodes: Node[], edges: Edge[], parentNodeId?: string) => {
const {
parallelList,
hasAbnormalEdges,
} = getParallelInfo(nodes, edges, parentNodeId)
const { workflowConfig } = workflowStore.getState()
// const {
// parallelList,
// hasAbnormalEdges,
// } = getParallelInfo(nodes, edges, parentNodeId)
// const { workflowConfig } = workflowStore.getState()
if (hasAbnormalEdges)
return false
// if (hasAbnormalEdges)
// return false
for (let i = 0; i < parallelList.length; i++) {
const parallel = parallelList[i]
// for (let i = 0; i < parallelList.length; i++) {
// const parallel = parallelList[i]
if (parallel.depth > (workflowConfig?.parallel_depth_limit || PARALLEL_DEPTH_LIMIT)) {
const { setShowTips } = workflowStore.getState()
setShowTips(t('workflow.common.parallelTip.depthLimit', { num: (workflowConfig?.parallel_depth_limit || PARALLEL_DEPTH_LIMIT) }))
return false
}
}
// if (parallel.depth > (workflowConfig?.parallel_depth_limit || PARALLEL_DEPTH_LIMIT)) {
// const { setShowTips } = workflowStore.getState()
// setShowTips(t('workflow.common.parallelTip.depthLimit', { num: (workflowConfig?.parallel_depth_limit || PARALLEL_DEPTH_LIMIT) }))
// return false
// }
// }
return true
}, [t, workflowStore])

View File

@ -131,7 +131,7 @@ const BaseNode: FC<BaseNodeProps> = ({
return (
<div
className={cn(
'flex rounded-2xl border-[2px]',
'relative flex rounded-2xl border',
showSelectedBorder ? 'border-components-option-card-option-selected-border' : 'border-transparent',
!showSelectedBorder && data._inParallelHovering && 'border-workflow-block-border-highlight',
data._waitingRun && 'opacity-70',
@ -142,6 +142,15 @@ const BaseNode: FC<BaseNodeProps> = ({
height: (data.type === BlockEnum.Iteration || data.type === BlockEnum.Loop) ? data.height : 'auto',
}}
>
{
data.type === BlockEnum.DataSource && (
<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'>
data source
</div>
</div>
)
}
<div
className={cn(
'group relative pb-1 shadow-xs',

View File

@ -38,6 +38,8 @@ import ListFilterNode from './list-operator/node'
import ListFilterPanel from './list-operator/panel'
import AgentNode from './agent/node'
import AgentPanel from './agent/panel'
import DataSourceNode from './data-source/node'
import DataSourcePanel from './data-source/panel'
import { TransferMethod } from '@/types/app'
export const NodeComponentMap: Record<string, ComponentType<any>> = {
@ -61,6 +63,7 @@ export const NodeComponentMap: Record<string, ComponentType<any>> = {
[BlockEnum.DocExtractor]: DocExtractorNode,
[BlockEnum.ListFilter]: ListFilterNode,
[BlockEnum.Agent]: AgentNode,
[BlockEnum.DataSource]: DataSourceNode,
}
export const PanelComponentMap: Record<string, ComponentType<any>> = {
@ -84,6 +87,7 @@ export const PanelComponentMap: Record<string, ComponentType<any>> = {
[BlockEnum.DocExtractor]: DocExtractorPanel,
[BlockEnum.ListFilter]: ListFilterPanel,
[BlockEnum.Agent]: AgentPanel,
[BlockEnum.DataSource]: DataSourcePanel,
}
export const CUSTOM_NODE_TYPE = 'custom'

View File

@ -0,0 +1,27 @@
import { BlockEnum } from '../../types'
import type { NodeDefault } from '../../types'
import type { DataSourceNodeType } from './types'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/blocks'
const nodeDefault: NodeDefault<DataSourceNodeType> = {
defaultValue: {
},
getAvailablePrevNodes(isChatMode: boolean) {
const nodes = isChatMode
? ALL_CHAT_AVAILABLE_BLOCKS
: ALL_COMPLETION_AVAILABLE_BLOCKS.filter(type => type !== BlockEnum.End)
return nodes
},
getAvailableNextNodes(isChatMode: boolean) {
const nodes = isChatMode ? ALL_CHAT_AVAILABLE_BLOCKS : ALL_COMPLETION_AVAILABLE_BLOCKS
return nodes
},
checkValid() {
return {
isValid: true,
errorMessage: '',
}
},
}
export default nodeDefault

View File

@ -0,0 +1,13 @@
import type { FC } from 'react'
import { memo } from 'react'
import type { DataSourceNodeType } from './types'
import type { NodeProps } from '@/app/components/workflow/types'
const Node: FC<NodeProps<DataSourceNodeType>> = () => {
return (
<div className='mb-1 px-3 py-1'>
DataSource
</div>
)
}
export default memo(Node)

View File

@ -0,0 +1,14 @@
import type { FC } from 'react'
import { memo } from 'react'
import type { DataSourceNodeType } from './types'
import type { NodePanelProps } from '@/app/components/workflow/types'
const Panel: FC<NodePanelProps<DataSourceNodeType>> = () => {
return (
<div className='mb-2 mt-2 space-y-4 px-4'>
datasource
</div>
)
}
export default memo(Panel)

View File

@ -0,0 +1,3 @@
import type { CommonNodeType } from '@/app/components/workflow/types'
export type DataSourceNodeType = CommonNodeType

View File

@ -40,6 +40,7 @@ export enum BlockEnum {
Loop = 'loop',
LoopStart = 'loop-start',
LoopEnd = 'loop-end',
DataSource = 'data-source',
}
export enum ControlMode {

View File

@ -229,6 +229,7 @@ const translation = {
'utilities': 'Utilities',
'noResult': 'No match found',
'agent': 'Agent Strategy',
'sources': 'Sources',
},
blocks: {
'start': 'Start',

View File

@ -230,6 +230,7 @@ const translation = {
'utilities': '工具',
'noResult': '未找到匹配项',
'agent': 'Agent 策略',
'sources': '数据源',
},
blocks: {
'start': '开始',

View File

@ -120,6 +120,7 @@ const config = {
'price-premium-text-background': 'var(--color-premium-text-background)',
'price-enterprise-background': 'var(--color-price-enterprise-background)',
'grid-mask-background': 'var(--color-grid-mask-background)',
'node-data-source-bg': 'var(--color-node-data-source-bg)',
},
animation: {
'spin-slow': 'spin 2s linear infinite',

View File

@ -61,4 +61,9 @@ html[data-theme="dark"] {
rgba(24, 24, 27, 0.08) 0%,
rgba(0, 0, 0, 0) 100%);
--color-line-divider-bg: linear-gradient(90deg, rgba(200, 206, 218, 0.14) 0%, rgba(0, 0, 0, 0) 100%, );
--workflow-block-wrapper-bg-1: rgba(39, 39, 43, 1);
--workflow-block-wrapper-bg-2: rgba(39, 39, 43, 0.2);
--color-node-data-source-bg: linear-gradient(100deg, var(--workflow-block-wrapper-bg-1, #E9EBF0) 0%, var(--workflow-block-wrapper-bg-2, rgba(233, 235, 240, 0.20)) 100%);
}

View File

@ -61,4 +61,7 @@ html[data-theme="light"] {
rgba(200, 206, 218, 0.2) 0%,
rgba(255, 255, 255, 0) 100%);
--color-line-divider-bg: linear-gradient(90deg, rgba(16, 24, 40, 0.08) 0%, rgba(255, 255, 255, 0) 100%);
--workflow-block-wrapper-bg-1: rgba(233, 235, 240, 1);
--workflow-block-wrapper-bg-2: rgba(233, 235, 240, 0.2);
--color-node-data-source-bg: linear-gradient(100deg, var(--workflow-block-wrapper-bg-1, #E9EBF0) 0%, var(--workflow-block-wrapper-bg-2, rgba(233, 235, 240, 0.20)) 100%);
}