available nodes

This commit is contained in:
StyleZhang 2024-03-15 13:24:09 +08:00
parent 8d9c86ac4c
commit 5fbf8ee6c6
17 changed files with 165 additions and 38 deletions

View File

@ -19,10 +19,12 @@ import Tooltip from '@/app/components/base/tooltip'
type BlocksProps = {
searchText: string
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
availableBlocksTypes?: BlockEnum[]
}
const Blocks = ({
searchText,
onSelect,
availableBlocksTypes = [],
}: BlocksProps) => {
const { t } = useTranslation()
const isChatMode = useIsChatMode()
@ -35,7 +37,7 @@ const Blocks = ({
if (block.type === BlockEnum.Answer && !isChatMode)
return false
return block.title.toLowerCase().includes(searchText.toLowerCase())
return block.title.toLowerCase().includes(searchText.toLowerCase()) && availableBlocksTypes.includes(block.type)
})
return {
@ -43,7 +45,7 @@ const Blocks = ({
[classification]: list,
}
}, {} as Record<string, typeof blocks>)
}, [blocks, isChatMode, searchText])
}, [blocks, isChatMode, searchText, availableBlocksTypes])
const isEmpty = Object.values(groups).every(list => !list.length)
const renderGroup = useCallback((classification: string) => {

View File

@ -12,6 +12,7 @@ import type {
OffsetOptions,
Placement,
} from '@floating-ui/react'
import type { BlockEnum, OnSelectBlock } from '../types'
import Tabs from './tabs'
import {
PortalToFollowElem,
@ -22,7 +23,6 @@ import {
Plus02,
SearchLg,
} from '@/app/components/base/icons/src/vender/line/general'
import type { OnSelectBlock } from '@/app/components/workflow/types'
import { XCircle } from '@/app/components/base/icons/src/vender/solid/general'
type NodeSelectorProps = {
@ -36,6 +36,7 @@ type NodeSelectorProps = {
triggerClassName?: (open: boolean) => string
popupClassName?: string
asChild?: boolean
availableBlocksTypes?: BlockEnum[]
}
const NodeSelector: FC<NodeSelectorProps> = ({
open: openFromProps,
@ -48,6 +49,7 @@ const NodeSelector: FC<NodeSelectorProps> = ({
triggerStyle,
popupClassName,
asChild,
availableBlocksTypes,
}) => {
const { t } = useTranslation()
const [searchText, setSearchText] = useState('')
@ -125,6 +127,7 @@ const NodeSelector: FC<NodeSelectorProps> = ({
<Tabs
onSelect={handleSelect}
searchText={searchText}
availableBlocksTypes={availableBlocksTypes}
/>
</div>
</PortalToFollowElemContent>

View File

@ -13,10 +13,12 @@ import Blocks from './blocks'
export type TabsProps = {
searchText: string
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
availableBlocksTypes?: BlockEnum[]
}
const Tabs: FC<TabsProps> = ({
searchText,
onSelect,
availableBlocksTypes,
}) => {
const tabs = useTabs()
const [activeTab, setActiveTab] = useState(tabs[0].key)
@ -46,6 +48,7 @@ const Tabs: FC<TabsProps> = ({
<Blocks
searchText={searchText}
onSelect={onSelect}
availableBlocksTypes={availableBlocksTypes}
/>
)
}

View File

@ -13,59 +13,128 @@ import ToolDefault from './nodes/tool/default'
import VariableAssignerDefault from './nodes/variable-assigner/default'
import EndNodeDefault from './nodes/end/default'
export const NODES_EXTRA_DATA = {
type NodesExtraData = {
author: string
about: string
availablePrevNodes: BlockEnum[]
availableNextNodes: BlockEnum[]
getAvailablePrevNodes: (isChatMode: boolean) => BlockEnum[]
getAvailableNextNodes: (isChatMode: boolean) => BlockEnum[]
checkValid: any
}
export const NODES_EXTRA_DATA: Record<BlockEnum, NodesExtraData> = {
[BlockEnum.Start]: {
author: 'Dify',
about: '',
availablePrevNodes: [],
availableNextNodes: [],
getAvailablePrevNodes: StartNodeDefault.getAvailablePrevNodes,
getAvailableNextNodes: StartNodeDefault.getAvailableNextNodes,
checkValid: StartNodeDefault.checkValid,
},
[BlockEnum.End]: {
author: 'Dify',
about: '',
availablePrevNodes: [],
availableNextNodes: [],
getAvailablePrevNodes: EndNodeDefault.getAvailablePrevNodes,
getAvailableNextNodes: EndNodeDefault.getAvailableNextNodes,
checkValid: EndNodeDefault.checkValid,
},
[BlockEnum.Answer]: {
author: 'Dify',
about: '',
availablePrevNodes: [],
availableNextNodes: [],
getAvailablePrevNodes: AnswerDefault.getAvailablePrevNodes,
getAvailableNextNodes: AnswerDefault.getAvailableNextNodes,
checkValid: AnswerDefault.checkValid,
},
[BlockEnum.LLM]: {
author: 'Dify',
about: '',
availablePrevNodes: [],
availableNextNodes: [],
getAvailablePrevNodes: LLMDefault.getAvailablePrevNodes,
getAvailableNextNodes: LLMDefault.getAvailableNextNodes,
checkValid: LLMDefault.checkValid,
},
[BlockEnum.KnowledgeRetrieval]: {
author: 'Dify',
about: '',
availablePrevNodes: [],
availableNextNodes: [],
getAvailablePrevNodes: KnowledgeRetrievalDefault.getAvailablePrevNodes,
getAvailableNextNodes: KnowledgeRetrievalDefault.getAvailableNextNodes,
checkValid: KnowledgeRetrievalDefault.checkValid,
},
[BlockEnum.IfElse]: {
author: 'Dify',
about: '',
availablePrevNodes: [],
availableNextNodes: [],
getAvailablePrevNodes: IfElseDefault.getAvailablePrevNodes,
getAvailableNextNodes: IfElseDefault.getAvailableNextNodes,
checkValid: IfElseDefault.checkValid,
},
[BlockEnum.Code]: {
author: 'Dify',
about: '',
availablePrevNodes: [],
availableNextNodes: [],
getAvailablePrevNodes: CodeDefault.getAvailablePrevNodes,
getAvailableNextNodes: CodeDefault.getAvailableNextNodes,
checkValid: CodeDefault.checkValid,
},
[BlockEnum.TemplateTransform]: {
author: 'Dify',
about: '',
availablePrevNodes: [],
availableNextNodes: [],
getAvailablePrevNodes: TemplateTransformDefault.getAvailablePrevNodes,
getAvailableNextNodes: TemplateTransformDefault.getAvailableNextNodes,
checkValid: TemplateTransformDefault.checkValid,
},
[BlockEnum.QuestionClassifier]: {
author: 'Dify',
about: '',
availablePrevNodes: [],
availableNextNodes: [],
getAvailablePrevNodes: QuestionClassifierDefault.getAvailablePrevNodes,
getAvailableNextNodes: QuestionClassifierDefault.getAvailableNextNodes,
checkValid: QuestionClassifierDefault.checkValid,
},
[BlockEnum.HttpRequest]: {
author: 'Dify',
about: '',
availablePrevNodes: [],
availableNextNodes: [],
getAvailablePrevNodes: HttpRequestDefault.getAvailablePrevNodes,
getAvailableNextNodes: HttpRequestDefault.getAvailableNextNodes,
checkValid: HttpRequestDefault.checkValid,
},
[BlockEnum.VariableAssigner]: {
author: 'Dify',
about: '',
availablePrevNodes: [],
availableNextNodes: [],
getAvailablePrevNodes: VariableAssignerDefault.getAvailablePrevNodes,
getAvailableNextNodes: VariableAssignerDefault.getAvailableNextNodes,
checkValid: VariableAssignerDefault.checkValid,
},
[BlockEnum.Tool]: {
author: 'Dify',
about: '',
availablePrevNodes: [],
availableNextNodes: [],
getAvailablePrevNodes: ToolDefault.getAvailablePrevNodes,
getAvailableNextNodes: ToolDefault.getAvailableNextNodes,
checkValid: ToolDefault.checkValid,
},
}
export const ALL_CHAT_AVAILABLE_BLOCKS = Object.keys(NODES_EXTRA_DATA).filter(key => key !== BlockEnum.End) as BlockEnum[]
export const ALL_COMPLETION_AVAILABLE_BLOCKS = Object.keys(NODES_EXTRA_DATA).filter(key => key !== BlockEnum.Answer) as BlockEnum[]
export const ALL_CHAT_AVAILABLE_BLOCKS = Object.keys(NODES_EXTRA_DATA).filter(key => key !== BlockEnum.End && key !== BlockEnum.Start) as BlockEnum[]
export const ALL_COMPLETION_AVAILABLE_BLOCKS = Object.keys(NODES_EXTRA_DATA).filter(key => key !== BlockEnum.Answer && key !== BlockEnum.Start) as BlockEnum[]
export const NODES_INITIAL_DATA = {
[BlockEnum.Start]: {

View File

@ -7,6 +7,7 @@ import {
NODES_INITIAL_DATA,
} from '../constants'
import { useStore } from '../store'
import { useIsChatMode } from './use-workflow'
export const useNodesInitialData = () => {
const { t } = useTranslation()
@ -28,10 +29,13 @@ export const useNodesInitialData = () => {
export const useNodesExtraData = () => {
const { t } = useTranslation()
const isChatMode = useIsChatMode()
return useMemo(() => produce(NODES_EXTRA_DATA, (draft) => {
Object.keys(draft).forEach((key) => {
draft[key as BlockEnum].about = t(`workflow.blocksAbout.${key}`)
draft[key as BlockEnum].availablePrevNodes = draft[key as BlockEnum].getAvailablePrevNodes(isChatMode)
draft[key as BlockEnum].availableNextNodes = draft[key as BlockEnum].getAvailableNextNodes(isChatMode)
})
}), [t])
}), [t, isChatMode])
}

View File

@ -96,13 +96,7 @@ export const useWorkflow = () => {
list.push(...incomers)
return list.filter((item) => {
if (item.data.type === BlockEnum.IfElse)
return false
if (item.data.type === BlockEnum.QuestionClassifier)
return false
return true
return SUPPORT_OUTPUT_VARS_NODE.includes(item.data.type)
})
}, [store])
@ -175,21 +169,16 @@ export const useWorkflow = () => {
return list
}, [store])
const getIncomersNodes = useCallback((currentNode: Node) => {
const {
getNodes,
edges,
} = store.getState()
return getIncomers(currentNode, getNodes(), edges)
}, [store])
const isValidConnection = useCallback(() => {
return true
}, [])
return {
handleLayout,
getTreeLeafNodes,
getBeforeNodesInSameBranch,
getAfterNodesInSameBranch,
getIncomersNodes,
isValidConnection,
}
}

View File

@ -10,6 +10,7 @@ import ReactFlow, {
Background,
ReactFlowProvider,
useOnViewportChange,
useStoreApi,
} from 'reactflow'
import type { Viewport } from 'reactflow'
import 'reactflow/dist/style.css'
@ -21,6 +22,7 @@ import {
useEdgesInteractions,
useNodesInteractions,
useNodesSyncDraft,
useWorkflow,
useWorkflowInit,
} from './hooks'
import Header from './header'
@ -60,6 +62,7 @@ const Workflow: FC<WorkflowProps> = memo(({
const showFeaturesPanel = useStore(state => state.showFeaturesPanel)
const runningStatus = useStore(s => s.runningStatus)
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
const store = useStoreApi()
useEffect(() => {
setAutoFreeze(false)
@ -84,6 +87,7 @@ const Workflow: FC<WorkflowProps> = memo(({
handleEdgeDelete,
handleEdgesChange,
} = useEdgesInteractions()
const { isValidConnection } = useWorkflow()
useOnViewportChange({
onEnd: () => handleSyncWorkflowDraft(),
@ -130,6 +134,7 @@ const Workflow: FC<WorkflowProps> = memo(({
zoomOnPinch={!runningStatus}
zoomOnScroll={!runningStatus}
zoomOnDoubleClick={!runningStatus}
isValidConnection={isValidConnection}
>
<Background
gap={[14, 14]}

View File

@ -3,23 +3,33 @@ import {
useCallback,
} from 'react'
import { useTranslation } from 'react-i18next'
import { useNodesInteractions } from '@/app/components/workflow/hooks'
import {
useNodesExtraData,
useNodesInteractions,
} from '@/app/components/workflow/hooks'
import BlockSelector from '@/app/components/workflow/block-selector'
import { Plus } from '@/app/components/base/icons/src/vender/line/general'
import type { OnSelectBlock } from '@/app/components/workflow/types'
import type {
BlockEnum,
OnSelectBlock,
} from '@/app/components/workflow/types'
type AddProps = {
nodeId: string
nodeType: BlockEnum
sourceHandle: string
branchName?: string
}
const Add = ({
nodeId,
nodeType,
sourceHandle,
branchName,
}: AddProps) => {
const { t } = useTranslation()
const { handleNodeAdd } = useNodesInteractions()
const nodesExtraData = useNodesExtraData()
const availableNextNodes = nodesExtraData[nodeType].availableNextNodes
const handleSelect = useCallback<OnSelectBlock>((type, toolDefaultValue) => {
handleNodeAdd(
@ -65,6 +75,7 @@ const Add = ({
offset={0}
trigger={renderTrigger}
popupClassName='!w-[328px]'
availableBlocksTypes={availableNextNodes}
/>
)
}

View File

@ -51,6 +51,7 @@ const NextStep = ({
!nodeWithBranches && !outgoers.length && (
<Add
nodeId={selectedNode!.id}
nodeType={selectedNode!.data.type}
sourceHandle='source'
/>
)
@ -81,6 +82,7 @@ const NextStep = ({
<Add
key={branch.id}
nodeId={selectedNode!.id}
nodeType={selectedNode!.data.type}
sourceHandle={branch.id}
branchName={branch.name}
/>

View File

@ -3,13 +3,17 @@ import {
useCallback,
} from 'react'
import { useTranslation } from 'react-i18next'
import { union } from 'lodash-es'
import type {
CommonNodeType,
OnSelectBlock,
} from '@/app/components/workflow/types'
import BlockIcon from '@/app/components/workflow/block-icon'
import BlockSelector from '@/app/components/workflow/block-selector'
import { useNodesInteractions } from '@/app/components/workflow/hooks'
import {
useNodesExtraData,
useNodesInteractions,
} from '@/app/components/workflow/hooks'
import Button from '@/app/components/base/button'
type ItemProps = {
@ -26,6 +30,9 @@ const Item = ({
}: ItemProps) => {
const { t } = useTranslation()
const { handleNodeChange } = useNodesInteractions()
const nodesExtraData = useNodesExtraData()
const availablePrevNodes = nodesExtraData[data.type].availablePrevNodes
const availableNextNodes = nodesExtraData[data.type].availableNextNodes
const handleSelect = useCallback<OnSelectBlock>((type, toolDefaultValue) => {
handleNodeChange(nodeId, type, sourceHandle, toolDefaultValue)
}, [nodeId, sourceHandle, handleNodeChange])
@ -68,6 +75,7 @@ const Item = ({
}}
trigger={renderTrigger}
popupClassName='!w-[328px]'
availableBlocksTypes={union(availablePrevNodes, availableNextNodes)}
/>
</div>
)

View File

@ -13,7 +13,10 @@ import { BlockEnum } from '../../../types'
import type { Node } from '../../../types'
import BlockSelector from '../../../block-selector'
import type { ToolDefaultValue } from '../../../block-selector/types'
import { useNodesInteractions } from '../../../hooks'
import {
useNodesExtraData,
useNodesInteractions,
} from '../../../hooks'
import { useStore } from '../../../store'
type NodeHandleProps = {
@ -31,7 +34,10 @@ export const NodeTargetHandle = memo(({
}: NodeHandleProps) => {
const [open, setOpen] = useState(false)
const { handleNodeAdd } = useNodesInteractions()
const nodesExtraData = useNodesExtraData()
const connected = data._connectedTargetHandleIds?.includes(handleId)
const availablePrevNodes = nodesExtraData[data.type].availablePrevNodes
const isConnectable = !!availablePrevNodes.length
const handleOpenChange = useCallback((v: boolean) => {
setOpen(v)
@ -67,11 +73,11 @@ export const NodeTargetHandle = memo(({
${data.type === BlockEnum.Start && 'opacity-0'}
${handleClassName}
`}
isConnectable={data.type !== BlockEnum.Start}
isConnectable={isConnectable}
onClick={handleHandleClick}
>
{
!connected && data.type !== BlockEnum.Start && (
!connected && isConnectable && (
<BlockSelector
open={open}
onOpenChange={handleOpenChange}
@ -84,6 +90,7 @@ export const NodeTargetHandle = memo(({
group-hover:flex
${open && '!flex'}
`}
availableBlocksTypes={availablePrevNodes}
/>
)
}
@ -103,6 +110,9 @@ export const NodeSourceHandle = memo(({
const notInitialWorkflow = useStore(s => s.notInitialWorkflow)
const [open, setOpen] = useState(false)
const { handleNodeAdd } = useNodesInteractions()
const nodesExtraData = useNodesExtraData()
const availableNextNodes = nodesExtraData[data.type].availableNextNodes
const isConnectable = !!availableNextNodes.length
const connected = data._connectedSourceHandleIds?.includes(handleId)
const handleOpenChange = useCallback((v: boolean) => {
setOpen(v)
@ -142,10 +152,11 @@ export const NodeSourceHandle = memo(({
${!connected && 'after:opacity-0'}
${handleClassName}
`}
isConnectable={isConnectable}
onClick={handleHandleClick}
>
{
!connected && (
!connected && isConnectable && (
<BlockSelector
open={open}
onOpenChange={handleOpenChange}
@ -157,6 +168,7 @@ export const NodeSourceHandle = memo(({
group-hover:flex
${open && '!flex'}
`}
availableBlocksTypes={availableNextNodes}
/>
)
}

View File

@ -3,20 +3,32 @@ import {
useCallback,
} from 'react'
import { useTranslation } from 'react-i18next'
import { union } from 'lodash-es'
import BlockSelector from '@/app/components/workflow/block-selector'
import { useNodesInteractions } from '@/app/components/workflow/hooks'
import type { OnSelectBlock } from '@/app/components/workflow/types'
import {
useNodesExtraData,
useNodesInteractions,
} from '@/app/components/workflow/hooks'
import type {
BlockEnum,
OnSelectBlock,
} from '@/app/components/workflow/types'
type ChangeBlockProps = {
nodeId: string
nodeType: BlockEnum
sourceHandle: string
}
const ChangeBlock = ({
nodeId,
nodeType,
sourceHandle,
}: ChangeBlockProps) => {
const { t } = useTranslation()
const { handleNodeChange } = useNodesInteractions()
const nodesExtraData = useNodesExtraData()
const availablePrevNodes = nodesExtraData[nodeType].availablePrevNodes
const availableNextNodes = nodesExtraData[nodeType].availableNextNodes
const handleSelect = useCallback<OnSelectBlock>((type, toolDefaultValue) => {
handleNodeChange(nodeId, type, sourceHandle, toolDefaultValue)
@ -39,6 +51,7 @@ const ChangeBlock = ({
}}
onSelect={handleSelect}
trigger={renderTrigger}
availableBlocksTypes={union(availablePrevNodes, availableNextNodes)}
/>
)
}

View File

@ -129,6 +129,7 @@ const PanelOperator = ({
data.type !== BlockEnum.Start && (
<ChangeBlock
nodeId={id}
nodeType={data.type}
sourceHandle={edge?.sourceHandle || 'source'}
/>
)

View File

@ -20,6 +20,7 @@ import {
import BlockIcon from '@/app/components/workflow/block-icon'
import {
useNodeDataUpdate,
useNodesExtraData,
useNodesInteractions,
} from '@/app/components/workflow/hooks'
import { canRunBySingle } from '@/app/components/workflow/utils'
@ -27,7 +28,6 @@ import { GitBranch01 } from '@/app/components/base/icons/src/vender/line/develop
import { Play } from '@/app/components/base/icons/src/vender/line/mediaAndDevices'
import TooltipPlus from '@/app/components/base/tooltip-plus'
import type { Node } from '@/app/components/workflow/types'
import { BlockEnum } from '@/app/components/workflow/types'
type BasePanelProps = {
children: ReactElement
@ -40,6 +40,9 @@ const BasePanel: FC<BasePanelProps> = ({
}) => {
const { t } = useTranslation()
const { handleNodeSelect } = useNodesInteractions()
const nodesExtraData = useNodesExtraData()
const availableNextNodes = nodesExtraData[data.type].availableNextNodes
const {
handleNodeDataUpdate,
handleNodeDataUpdateWithSyncDraft,
@ -102,7 +105,7 @@ const BasePanel: FC<BasePanelProps> = ({
{cloneElement(children, { id, data })}
</div>
{
data.type !== BlockEnum.End && (
!!availableNextNodes.length && (
<div className='p-4 border-t-[0.5px] border-t-black/5'>
<div className='flex items-center mb-1 text-gray-700 text-[13px] font-semibold'>
<GitBranch01 className='mr-1 w-4 h-4' />

View File

@ -1,4 +1,4 @@
import type { NodeDefault } from '../../types'
import { BlockEnum, type NodeDefault } from '../../types'
import { type IfElseNodeType, LogicalOperator } from './types'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
@ -23,7 +23,7 @@ const nodeDefault: NodeDefault<IfElseNodeType> = {
},
getAvailableNextNodes(isChatMode: boolean) {
const nodes = isChatMode ? ALL_CHAT_AVAILABLE_BLOCKS : ALL_COMPLETION_AVAILABLE_BLOCKS
return nodes
return nodes.filter(type => type !== BlockEnum.VariableAssigner)
},
checkValid(payload: IfElseNodeType) {
let isValid = true

View File

@ -1,4 +1,5 @@
import type { NodeDefault } from '../../types'
import { BlockEnum } from '../../types'
import type { QuestionClassifierNodeType } from './types'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
@ -21,7 +22,7 @@ const nodeDefault: NodeDefault<QuestionClassifierNodeType> = {
},
getAvailableNextNodes(isChatMode: boolean) {
const nodes = isChatMode ? ALL_CHAT_AVAILABLE_BLOCKS : ALL_COMPLETION_AVAILABLE_BLOCKS
return nodes
return nodes.filter(type => type !== BlockEnum.VariableAssigner)
},
checkValid(payload: QuestionClassifierNodeType) {
let isValid = true

View File

@ -1,4 +1,5 @@
import { type NodeDefault, VarType } from '../../types'
import { BlockEnum } from '../../types'
import type { VariableAssignerNodeType } from './types'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants'
@ -9,7 +10,7 @@ const nodeDefault: NodeDefault<VariableAssignerNodeType> = {
},
getAvailablePrevNodes(isChatMode: boolean) {
const nodes = isChatMode ? ALL_CHAT_AVAILABLE_BLOCKS : ALL_COMPLETION_AVAILABLE_BLOCKS
return nodes
return nodes.filter(type => type !== BlockEnum.IfElse && type !== BlockEnum.QuestionClassifier)
},
getAvailableNextNodes(isChatMode: boolean) {
const nodes = isChatMode ? ALL_CHAT_AVAILABLE_BLOCKS : ALL_COMPLETION_AVAILABLE_BLOCKS