dify
This commit is contained in:
26
dify/web/app/components/workflow/hooks/index.ts
Normal file
26
dify/web/app/components/workflow/hooks/index.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
export * from './use-edges-interactions'
|
||||
export * from './use-node-data-update'
|
||||
export * from './use-nodes-interactions'
|
||||
export * from './use-nodes-sync-draft'
|
||||
export * from './use-workflow'
|
||||
export * from './use-workflow-run'
|
||||
export * from './use-checklist'
|
||||
export * from './use-selection-interactions'
|
||||
export * from './use-panel-interactions'
|
||||
export * from './use-workflow-start-run'
|
||||
export * from './use-nodes-layout'
|
||||
export * from './use-workflow-history'
|
||||
export * from './use-workflow-variables'
|
||||
export * from './use-shortcuts'
|
||||
export * from './use-workflow-interactions'
|
||||
export * from './use-workflow-mode'
|
||||
export * from './use-nodes-meta-data'
|
||||
export * from './use-available-blocks'
|
||||
export * from './use-workflow-refresh-draft'
|
||||
export * from './use-tool-icon'
|
||||
export * from './use-DSL'
|
||||
export * from './use-inspect-vars-crud'
|
||||
export * from './use-set-workflow-vars-with-value'
|
||||
export * from './use-workflow-search'
|
||||
export * from './use-auto-generate-webhook-url'
|
||||
export * from './use-serial-async-callback'
|
||||
11
dify/web/app/components/workflow/hooks/use-DSL.ts
Normal file
11
dify/web/app/components/workflow/hooks/use-DSL.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { useHooksStore } from '@/app/components/workflow/hooks-store'
|
||||
|
||||
export const useDSL = () => {
|
||||
const exportCheck = useHooksStore(s => s.exportCheck)
|
||||
const handleExportDSL = useHooksStore(s => s.handleExportDSL)
|
||||
|
||||
return {
|
||||
exportCheck,
|
||||
handleExportDSL,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { useCallback } from 'react'
|
||||
import { produce } from 'immer'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { fetchWebhookUrl } from '@/service/apps'
|
||||
|
||||
export const useAutoGenerateWebhookUrl = () => {
|
||||
const reactFlowStore = useStoreApi()
|
||||
|
||||
return useCallback(async (nodeId: string) => {
|
||||
const appId = useAppStore.getState().appDetail?.id
|
||||
if (!appId)
|
||||
return
|
||||
|
||||
const { getNodes } = reactFlowStore.getState()
|
||||
const node = getNodes().find(n => n.id === nodeId)
|
||||
if (!node || node.data.type !== BlockEnum.TriggerWebhook)
|
||||
return
|
||||
|
||||
if (node.data.webhook_url && node.data.webhook_url.length > 0)
|
||||
return
|
||||
|
||||
try {
|
||||
const response = await fetchWebhookUrl({ appId, nodeId })
|
||||
const { getNodes: getLatestNodes, setNodes } = reactFlowStore.getState()
|
||||
let hasUpdated = false
|
||||
const updatedNodes = produce(getLatestNodes(), (draft) => {
|
||||
const targetNode = draft.find(n => n.id === nodeId)
|
||||
if (!targetNode || targetNode.data.type !== BlockEnum.TriggerWebhook)
|
||||
return
|
||||
|
||||
targetNode.data = {
|
||||
...targetNode.data,
|
||||
webhook_url: response.webhook_url,
|
||||
webhook_debug_url: response.webhook_debug_url,
|
||||
}
|
||||
hasUpdated = true
|
||||
})
|
||||
|
||||
if (hasUpdated)
|
||||
setNodes(updatedNodes)
|
||||
}
|
||||
catch (error: unknown) {
|
||||
console.error('Failed to auto-generate webhook URL:', error)
|
||||
}
|
||||
}, [reactFlowStore])
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import {
|
||||
useCallback,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import { BlockEnum } from '../types'
|
||||
import { useNodesMetaData } from './use-nodes-meta-data'
|
||||
|
||||
const availableBlocksFilter = (nodeType: BlockEnum, inContainer?: boolean) => {
|
||||
if (inContainer && (nodeType === BlockEnum.Iteration || nodeType === BlockEnum.Loop || nodeType === BlockEnum.End || nodeType === BlockEnum.DataSource || nodeType === BlockEnum.KnowledgeBase))
|
||||
return false
|
||||
|
||||
if (!inContainer && nodeType === BlockEnum.LoopEnd)
|
||||
return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export const useAvailableBlocks = (nodeType?: BlockEnum, inContainer?: boolean) => {
|
||||
const {
|
||||
nodes: availableNodes,
|
||||
} = useNodesMetaData()
|
||||
const availableNodesType = useMemo(() => availableNodes.map(node => node.metaData.type), [availableNodes])
|
||||
const availablePrevBlocks = useMemo(() => {
|
||||
if (!nodeType || nodeType === BlockEnum.Start || nodeType === BlockEnum.DataSource
|
||||
|| nodeType === BlockEnum.TriggerPlugin || nodeType === BlockEnum.TriggerWebhook
|
||||
|| nodeType === BlockEnum.TriggerSchedule)
|
||||
return []
|
||||
|
||||
return availableNodesType
|
||||
}, [availableNodesType, nodeType])
|
||||
const availableNextBlocks = useMemo(() => {
|
||||
if (!nodeType || nodeType === BlockEnum.End || nodeType === BlockEnum.LoopEnd || nodeType === BlockEnum.KnowledgeBase)
|
||||
return []
|
||||
|
||||
return availableNodesType
|
||||
}, [availableNodesType, nodeType])
|
||||
|
||||
const getAvailableBlocks = useCallback((nodeType?: BlockEnum, inContainer?: boolean) => {
|
||||
let availablePrevBlocks = availableNodesType
|
||||
if (!nodeType || nodeType === BlockEnum.Start || nodeType === BlockEnum.DataSource)
|
||||
availablePrevBlocks = []
|
||||
|
||||
let availableNextBlocks = availableNodesType
|
||||
if (!nodeType || nodeType === BlockEnum.End || nodeType === BlockEnum.LoopEnd || nodeType === BlockEnum.KnowledgeBase)
|
||||
availableNextBlocks = []
|
||||
|
||||
return {
|
||||
availablePrevBlocks: availablePrevBlocks.filter(nType => availableBlocksFilter(nType, inContainer)),
|
||||
availableNextBlocks: availableNextBlocks.filter(nType => availableBlocksFilter(nType, inContainer)),
|
||||
}
|
||||
}, [availableNodesType])
|
||||
|
||||
return useMemo(() => {
|
||||
return {
|
||||
getAvailableBlocks,
|
||||
availablePrevBlocks: availablePrevBlocks.filter(nType => availableBlocksFilter(nType, inContainer)),
|
||||
availableNextBlocks: availableNextBlocks.filter(nType => availableBlocksFilter(nType, inContainer)),
|
||||
}
|
||||
}, [getAvailableBlocks, availablePrevBlocks, availableNextBlocks, inContainer])
|
||||
}
|
||||
429
dify/web/app/components/workflow/hooks/use-checklist.ts
Normal file
429
dify/web/app/components/workflow/hooks/use-checklist.ts
Normal file
@@ -0,0 +1,429 @@
|
||||
import {
|
||||
useCallback,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useEdges, useStoreApi } from 'reactflow'
|
||||
import type {
|
||||
CommonEdgeType,
|
||||
CommonNodeType,
|
||||
Edge,
|
||||
Node,
|
||||
ValueSelector,
|
||||
} from '../types'
|
||||
import { BlockEnum } from '../types'
|
||||
import {
|
||||
useStore,
|
||||
useWorkflowStore,
|
||||
} from '../store'
|
||||
import {
|
||||
getDataSourceCheckParams,
|
||||
getToolCheckParams,
|
||||
getValidTreeNodes,
|
||||
} from '../utils'
|
||||
import { getTriggerCheckParams } from '../utils/trigger'
|
||||
import {
|
||||
CUSTOM_NODE,
|
||||
} from '../constants'
|
||||
import {
|
||||
useGetToolIcon,
|
||||
useNodesMetaData,
|
||||
} from '../hooks'
|
||||
import type { ToolNodeType } from '../nodes/tool/types'
|
||||
import type { DataSourceNodeType } from '../nodes/data-source/types'
|
||||
import type { PluginTriggerNodeType } from '../nodes/trigger-plugin/types'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import type { AgentNodeType } from '../nodes/agent/types'
|
||||
import { useStrategyProviders } from '@/service/use-strategy'
|
||||
import { useAllTriggerPlugins } from '@/service/use-triggers'
|
||||
import { useDatasetsDetailStore } from '../datasets-detail-store/store'
|
||||
import type { KnowledgeRetrievalNodeType } from '../nodes/knowledge-retrieval/types'
|
||||
import type { DataSet } from '@/models/datasets'
|
||||
import { fetchDatasets } from '@/service/datasets'
|
||||
import { MAX_TREE_DEPTH } from '@/config'
|
||||
import useNodesAvailableVarList, { useGetNodesAvailableVarList } from './use-nodes-available-var-list'
|
||||
import { getNodeUsedVars, isSpecialVar } from '../nodes/_base/components/variable/utils'
|
||||
import type { Emoji } from '@/app/components/tools/types'
|
||||
import { useModelList } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import type { KnowledgeBaseNodeType } from '../nodes/knowledge-base/types'
|
||||
import {
|
||||
useAllBuiltInTools,
|
||||
useAllCustomTools,
|
||||
useAllWorkflowTools,
|
||||
} from '@/service/use-tools'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import useNodes from '@/app/components/workflow/store/workflow/use-nodes'
|
||||
|
||||
export type ChecklistItem = {
|
||||
id: string
|
||||
type: BlockEnum | string
|
||||
title: string
|
||||
toolIcon?: string | Emoji
|
||||
unConnected?: boolean
|
||||
errorMessage?: string
|
||||
canNavigate: boolean
|
||||
}
|
||||
|
||||
const START_NODE_TYPES: BlockEnum[] = [
|
||||
BlockEnum.Start,
|
||||
BlockEnum.TriggerSchedule,
|
||||
BlockEnum.TriggerWebhook,
|
||||
BlockEnum.TriggerPlugin,
|
||||
]
|
||||
|
||||
export const useChecklist = (nodes: Node[], edges: Edge[]) => {
|
||||
const { t } = useTranslation()
|
||||
const language = useGetLanguage()
|
||||
const { nodesMap: nodesExtraData } = useNodesMetaData()
|
||||
const { data: buildInTools } = useAllBuiltInTools()
|
||||
const { data: customTools } = useAllCustomTools()
|
||||
const { data: workflowTools } = useAllWorkflowTools()
|
||||
const dataSourceList = useStore(s => s.dataSourceList)
|
||||
const { data: strategyProviders } = useStrategyProviders()
|
||||
const { data: triggerPlugins } = useAllTriggerPlugins()
|
||||
const datasetsDetail = useDatasetsDetailStore(s => s.datasetsDetail)
|
||||
const getToolIcon = useGetToolIcon()
|
||||
const appMode = useAppStore.getState().appDetail?.mode
|
||||
const shouldCheckStartNode = appMode === AppModeEnum.WORKFLOW || appMode === AppModeEnum.ADVANCED_CHAT
|
||||
|
||||
const map = useNodesAvailableVarList(nodes)
|
||||
const { data: embeddingModelList } = useModelList(ModelTypeEnum.textEmbedding)
|
||||
const { data: rerankModelList } = useModelList(ModelTypeEnum.rerank)
|
||||
|
||||
const getCheckData = useCallback((data: CommonNodeType<{}>) => {
|
||||
let checkData = data
|
||||
if (data.type === BlockEnum.KnowledgeRetrieval) {
|
||||
const datasetIds = (data as CommonNodeType<KnowledgeRetrievalNodeType>).dataset_ids
|
||||
const _datasets = datasetIds.reduce<DataSet[]>((acc, id) => {
|
||||
if (datasetsDetail[id])
|
||||
acc.push(datasetsDetail[id])
|
||||
return acc
|
||||
}, [])
|
||||
checkData = {
|
||||
...data,
|
||||
_datasets,
|
||||
} as CommonNodeType<KnowledgeRetrievalNodeType>
|
||||
}
|
||||
else if (data.type === BlockEnum.KnowledgeBase) {
|
||||
checkData = {
|
||||
...data,
|
||||
_embeddingModelList: embeddingModelList,
|
||||
_rerankModelList: rerankModelList,
|
||||
} as CommonNodeType<KnowledgeBaseNodeType>
|
||||
}
|
||||
return checkData
|
||||
}, [datasetsDetail, embeddingModelList, rerankModelList])
|
||||
|
||||
const needWarningNodes = useMemo<ChecklistItem[]>(() => {
|
||||
const list: ChecklistItem[] = []
|
||||
const filteredNodes = nodes.filter(node => node.type === CUSTOM_NODE)
|
||||
const { validNodes } = getValidTreeNodes(filteredNodes, edges)
|
||||
|
||||
for (let i = 0; i < filteredNodes.length; i++) {
|
||||
const node = filteredNodes[i]
|
||||
let moreDataForCheckValid
|
||||
let usedVars: ValueSelector[] = []
|
||||
|
||||
if (node.data.type === BlockEnum.Tool)
|
||||
moreDataForCheckValid = getToolCheckParams(node.data as ToolNodeType, buildInTools || [], customTools || [], workflowTools || [], language)
|
||||
|
||||
if (node.data.type === BlockEnum.DataSource)
|
||||
moreDataForCheckValid = getDataSourceCheckParams(node.data as DataSourceNodeType, dataSourceList || [], language)
|
||||
|
||||
if (node.data.type === BlockEnum.TriggerPlugin)
|
||||
moreDataForCheckValid = getTriggerCheckParams(node.data as PluginTriggerNodeType, triggerPlugins, language)
|
||||
|
||||
const toolIcon = getToolIcon(node.data)
|
||||
if (node.data.type === BlockEnum.Agent) {
|
||||
const data = node.data as AgentNodeType
|
||||
const isReadyForCheckValid = !!strategyProviders
|
||||
const provider = strategyProviders?.find(provider => provider.declaration.identity.name === data.agent_strategy_provider_name)
|
||||
const strategy = provider?.declaration.strategies?.find(s => s.identity.name === data.agent_strategy_name)
|
||||
moreDataForCheckValid = {
|
||||
provider,
|
||||
strategy,
|
||||
language,
|
||||
isReadyForCheckValid,
|
||||
}
|
||||
}
|
||||
else {
|
||||
usedVars = getNodeUsedVars(node).filter(v => v.length > 0)
|
||||
}
|
||||
|
||||
if (node.type === CUSTOM_NODE) {
|
||||
const checkData = getCheckData(node.data)
|
||||
const validator = nodesExtraData?.[node.data.type as BlockEnum]?.checkValid
|
||||
let errorMessage = validator ? validator(checkData, t, moreDataForCheckValid).errorMessage : undefined
|
||||
|
||||
if (!errorMessage) {
|
||||
const availableVars = map[node.id].availableVars
|
||||
|
||||
for (const variable of usedVars) {
|
||||
const isSpecialVars = isSpecialVar(variable[0])
|
||||
if (!isSpecialVars) {
|
||||
const usedNode = availableVars.find(v => v.nodeId === variable?.[0])
|
||||
if (usedNode) {
|
||||
const usedVar = usedNode.vars.find(v => v.variable === variable?.[1])
|
||||
if (!usedVar)
|
||||
errorMessage = t('workflow.errorMsg.invalidVariable')
|
||||
}
|
||||
else {
|
||||
errorMessage = t('workflow.errorMsg.invalidVariable')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Start nodes and Trigger nodes should not show unConnected error if they have validation errors
|
||||
// or if they are valid start nodes (even without incoming connections)
|
||||
const isStartNodeMeta = nodesExtraData?.[node.data.type as BlockEnum]?.metaData.isStart ?? false
|
||||
const canSkipConnectionCheck = shouldCheckStartNode ? isStartNodeMeta : true
|
||||
|
||||
const isUnconnected = !validNodes.find(n => n.id === node.id)
|
||||
const shouldShowError = errorMessage || (isUnconnected && !canSkipConnectionCheck)
|
||||
|
||||
if (shouldShowError) {
|
||||
list.push({
|
||||
id: node.id,
|
||||
type: node.data.type,
|
||||
title: node.data.title,
|
||||
toolIcon,
|
||||
unConnected: isUnconnected && !canSkipConnectionCheck,
|
||||
errorMessage,
|
||||
canNavigate: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for start nodes (including triggers)
|
||||
if (shouldCheckStartNode) {
|
||||
const startNodesFiltered = nodes.filter(node => START_NODE_TYPES.includes(node.data.type as BlockEnum))
|
||||
if (startNodesFiltered.length === 0) {
|
||||
list.push({
|
||||
id: 'start-node-required',
|
||||
type: BlockEnum.Start,
|
||||
title: t('workflow.panel.startNode'),
|
||||
errorMessage: t('workflow.common.needStartNode'),
|
||||
canNavigate: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const isRequiredNodesType = Object.keys(nodesExtraData!).filter((key: any) => (nodesExtraData as any)[key].metaData.isRequired)
|
||||
|
||||
isRequiredNodesType.forEach((type: string) => {
|
||||
if (!filteredNodes.find(node => node.data.type === type)) {
|
||||
list.push({
|
||||
id: `${type}-need-added`,
|
||||
type,
|
||||
title: t(`workflow.blocks.${type}`),
|
||||
errorMessage: t('workflow.common.needAdd', { node: t(`workflow.blocks.${type}`) }),
|
||||
canNavigate: false,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return list
|
||||
}, [nodes, nodesExtraData, edges, buildInTools, customTools, workflowTools, language, dataSourceList, getToolIcon, strategyProviders, getCheckData, t, map, shouldCheckStartNode])
|
||||
|
||||
return needWarningNodes
|
||||
}
|
||||
|
||||
export const useChecklistBeforePublish = () => {
|
||||
const { t } = useTranslation()
|
||||
const language = useGetLanguage()
|
||||
const { notify } = useToastContext()
|
||||
const store = useStoreApi()
|
||||
const { nodesMap: nodesExtraData } = useNodesMetaData()
|
||||
const { data: strategyProviders } = useStrategyProviders()
|
||||
const updateDatasetsDetail = useDatasetsDetailStore(s => s.updateDatasetsDetail)
|
||||
const updateTime = useRef(0)
|
||||
const workflowStore = useWorkflowStore()
|
||||
const { getNodesAvailableVarList } = useGetNodesAvailableVarList()
|
||||
const { data: embeddingModelList } = useModelList(ModelTypeEnum.textEmbedding)
|
||||
const { data: rerankModelList } = useModelList(ModelTypeEnum.rerank)
|
||||
const { data: buildInTools } = useAllBuiltInTools()
|
||||
const { data: customTools } = useAllCustomTools()
|
||||
const { data: workflowTools } = useAllWorkflowTools()
|
||||
const appMode = useAppStore.getState().appDetail?.mode
|
||||
const shouldCheckStartNode = appMode === AppModeEnum.WORKFLOW || appMode === AppModeEnum.ADVANCED_CHAT
|
||||
|
||||
const getCheckData = useCallback((data: CommonNodeType<{}>, datasets: DataSet[]) => {
|
||||
let checkData = data
|
||||
if (data.type === BlockEnum.KnowledgeRetrieval) {
|
||||
const datasetIds = (data as CommonNodeType<KnowledgeRetrievalNodeType>).dataset_ids
|
||||
const datasetsDetail = datasets.reduce<Record<string, DataSet>>((acc, dataset) => {
|
||||
acc[dataset.id] = dataset
|
||||
return acc
|
||||
}, {})
|
||||
const _datasets = datasetIds.reduce<DataSet[]>((acc, id) => {
|
||||
if (datasetsDetail[id])
|
||||
acc.push(datasetsDetail[id])
|
||||
return acc
|
||||
}, [])
|
||||
checkData = {
|
||||
...data,
|
||||
_datasets,
|
||||
} as CommonNodeType<KnowledgeRetrievalNodeType>
|
||||
}
|
||||
else if (data.type === BlockEnum.KnowledgeBase) {
|
||||
checkData = {
|
||||
...data,
|
||||
_embeddingModelList: embeddingModelList,
|
||||
_rerankModelList: rerankModelList,
|
||||
} as CommonNodeType<KnowledgeBaseNodeType>
|
||||
}
|
||||
return checkData
|
||||
}, [embeddingModelList, rerankModelList])
|
||||
|
||||
const handleCheckBeforePublish = useCallback(async () => {
|
||||
const {
|
||||
getNodes,
|
||||
edges,
|
||||
} = store.getState()
|
||||
const {
|
||||
dataSourceList,
|
||||
} = workflowStore.getState()
|
||||
const nodes = getNodes()
|
||||
const filteredNodes = nodes.filter(node => node.type === CUSTOM_NODE)
|
||||
const { validNodes, maxDepth } = getValidTreeNodes(filteredNodes, edges)
|
||||
|
||||
if (maxDepth > MAX_TREE_DEPTH) {
|
||||
notify({ type: 'error', message: t('workflow.common.maxTreeDepth', { depth: MAX_TREE_DEPTH }) })
|
||||
return false
|
||||
}
|
||||
// Before publish, we need to fetch datasets detail, in case of the settings of datasets have been changed
|
||||
const knowledgeRetrievalNodes = filteredNodes.filter(node => node.data.type === BlockEnum.KnowledgeRetrieval)
|
||||
const allDatasetIds = knowledgeRetrievalNodes.reduce<string[]>((acc, node) => {
|
||||
return Array.from(new Set([...acc, ...(node.data as CommonNodeType<KnowledgeRetrievalNodeType>).dataset_ids]))
|
||||
}, [])
|
||||
let datasets: DataSet[] = []
|
||||
if (allDatasetIds.length > 0) {
|
||||
updateTime.current = updateTime.current + 1
|
||||
const currUpdateTime = updateTime.current
|
||||
const { data: datasetsDetail } = await fetchDatasets({ url: '/datasets', params: { page: 1, ids: allDatasetIds } })
|
||||
if (datasetsDetail && datasetsDetail.length > 0) {
|
||||
// avoid old data to overwrite the new data
|
||||
if (currUpdateTime < updateTime.current)
|
||||
return false
|
||||
datasets = datasetsDetail
|
||||
updateDatasetsDetail(datasetsDetail)
|
||||
}
|
||||
}
|
||||
const map = getNodesAvailableVarList(nodes)
|
||||
for (let i = 0; i < filteredNodes.length; i++) {
|
||||
const node = filteredNodes[i]
|
||||
let moreDataForCheckValid
|
||||
let usedVars: ValueSelector[] = []
|
||||
if (node.data.type === BlockEnum.Tool)
|
||||
moreDataForCheckValid = getToolCheckParams(node.data as ToolNodeType, buildInTools || [], customTools || [], workflowTools || [], language)
|
||||
|
||||
if (node.data.type === BlockEnum.DataSource)
|
||||
moreDataForCheckValid = getDataSourceCheckParams(node.data as DataSourceNodeType, dataSourceList || [], language)
|
||||
|
||||
if (node.data.type === BlockEnum.Agent) {
|
||||
const data = node.data as AgentNodeType
|
||||
const isReadyForCheckValid = !!strategyProviders
|
||||
const provider = strategyProviders?.find(provider => provider.declaration.identity.name === data.agent_strategy_provider_name)
|
||||
const strategy = provider?.declaration.strategies?.find(s => s.identity.name === data.agent_strategy_name)
|
||||
moreDataForCheckValid = {
|
||||
provider,
|
||||
strategy,
|
||||
language,
|
||||
isReadyForCheckValid,
|
||||
}
|
||||
}
|
||||
else {
|
||||
usedVars = getNodeUsedVars(node).filter(v => v.length > 0)
|
||||
}
|
||||
const checkData = getCheckData(node.data, datasets)
|
||||
const { errorMessage } = nodesExtraData![node.data.type as BlockEnum].checkValid(checkData, t, moreDataForCheckValid)
|
||||
|
||||
if (errorMessage) {
|
||||
notify({ type: 'error', message: `[${node.data.title}] ${errorMessage}` })
|
||||
return false
|
||||
}
|
||||
|
||||
const availableVars = map[node.id].availableVars
|
||||
|
||||
for (const variable of usedVars) {
|
||||
const isSpecialVars = isSpecialVar(variable[0])
|
||||
if (!isSpecialVars) {
|
||||
const usedNode = availableVars.find(v => v.nodeId === variable?.[0])
|
||||
if (usedNode) {
|
||||
const usedVar = usedNode.vars.find(v => v.variable === variable?.[1])
|
||||
if (!usedVar) {
|
||||
notify({ type: 'error', message: `[${node.data.title}] ${t('workflow.errorMsg.invalidVariable')}` })
|
||||
return false
|
||||
}
|
||||
}
|
||||
else {
|
||||
notify({ type: 'error', message: `[${node.data.title}] ${t('workflow.errorMsg.invalidVariable')}` })
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const isStartNodeMeta = nodesExtraData?.[node.data.type as BlockEnum]?.metaData.isStart ?? false
|
||||
const canSkipConnectionCheck = shouldCheckStartNode ? isStartNodeMeta : true
|
||||
const isUnconnected = !validNodes.find(n => n.id === node.id)
|
||||
|
||||
if (isUnconnected && !canSkipConnectionCheck) {
|
||||
notify({ type: 'error', message: `[${node.data.title}] ${t('workflow.common.needConnectTip')}` })
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldCheckStartNode) {
|
||||
const startNodesFiltered = nodes.filter(node => START_NODE_TYPES.includes(node.data.type as BlockEnum))
|
||||
if (startNodesFiltered.length === 0) {
|
||||
notify({ type: 'error', message: t('workflow.common.needStartNode') })
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const isRequiredNodesType = Object.keys(nodesExtraData!).filter((key: any) => (nodesExtraData as any)[key].metaData.isRequired)
|
||||
|
||||
for (let i = 0; i < isRequiredNodesType.length; i++) {
|
||||
const type = isRequiredNodesType[i]
|
||||
|
||||
if (!filteredNodes.find(node => node.data.type === type)) {
|
||||
notify({ type: 'error', message: t('workflow.common.needAdd', { node: t(`workflow.blocks.${type}`) }) })
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}, [store, notify, t, language, nodesExtraData, strategyProviders, updateDatasetsDetail, getCheckData, workflowStore, buildInTools, customTools, workflowTools, shouldCheckStartNode])
|
||||
|
||||
return {
|
||||
handleCheckBeforePublish,
|
||||
}
|
||||
}
|
||||
|
||||
export const useWorkflowRunValidation = () => {
|
||||
const { t } = useTranslation()
|
||||
const nodes = useNodes()
|
||||
const edges = useEdges<CommonEdgeType>()
|
||||
const needWarningNodes = useChecklist(nodes, edges)
|
||||
const { notify } = useToastContext()
|
||||
|
||||
const validateBeforeRun = useCallback(() => {
|
||||
if (needWarningNodes.length > 0) {
|
||||
notify({ type: 'error', message: t('workflow.panel.checklistTip') })
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}, [needWarningNodes, notify, t])
|
||||
|
||||
return {
|
||||
validateBeforeRun,
|
||||
hasValidationErrors: needWarningNodes.length > 0,
|
||||
warningNodes: needWarningNodes,
|
||||
}
|
||||
}
|
||||
88
dify/web/app/components/workflow/hooks/use-config-vision.ts
Normal file
88
dify/web/app/components/workflow/hooks/use-config-vision.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { produce } from 'immer'
|
||||
import { useCallback } from 'react'
|
||||
import { useIsChatMode } from './use-workflow'
|
||||
import type { ModelConfig, VisionSetting } from '@/app/components/workflow/types'
|
||||
import { useTextGenerationCurrentProviderAndModelAndModelList } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import {
|
||||
ModelFeatureEnum,
|
||||
} from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { Resolution } from '@/types/app'
|
||||
|
||||
type Payload = {
|
||||
enabled: boolean
|
||||
configs?: VisionSetting
|
||||
}
|
||||
|
||||
type Params = {
|
||||
payload: Payload
|
||||
onChange: (payload: Payload) => void
|
||||
}
|
||||
const useConfigVision = (model: ModelConfig, {
|
||||
payload = {
|
||||
enabled: false,
|
||||
},
|
||||
onChange,
|
||||
}: Params) => {
|
||||
const {
|
||||
currentModel: currModel,
|
||||
} = useTextGenerationCurrentProviderAndModelAndModelList(
|
||||
{
|
||||
provider: model.provider,
|
||||
model: model.name,
|
||||
},
|
||||
)
|
||||
|
||||
const isChatMode = useIsChatMode()
|
||||
|
||||
const getIsVisionModel = useCallback(() => {
|
||||
return !!currModel?.features?.includes(ModelFeatureEnum.vision)
|
||||
}, [currModel])
|
||||
|
||||
const isVisionModel = getIsVisionModel()
|
||||
|
||||
const handleVisionResolutionEnabledChange = useCallback((enabled: boolean) => {
|
||||
const newPayload = produce(payload, (draft) => {
|
||||
draft.enabled = enabled
|
||||
if (enabled && isChatMode) {
|
||||
draft.configs = {
|
||||
detail: Resolution.high,
|
||||
variable_selector: ['sys', 'files'],
|
||||
}
|
||||
}
|
||||
})
|
||||
onChange(newPayload)
|
||||
}, [isChatMode, onChange, payload])
|
||||
|
||||
const handleVisionResolutionChange = useCallback((config: VisionSetting) => {
|
||||
const newPayload = produce(payload, (draft) => {
|
||||
draft.configs = config
|
||||
})
|
||||
onChange(newPayload)
|
||||
}, [onChange, payload])
|
||||
|
||||
const handleModelChanged = useCallback(() => {
|
||||
const isVisionModel = getIsVisionModel()
|
||||
if (!isVisionModel) {
|
||||
handleVisionResolutionEnabledChange(false)
|
||||
return
|
||||
}
|
||||
if (payload.enabled) {
|
||||
onChange({
|
||||
enabled: true,
|
||||
configs: {
|
||||
detail: Resolution.high,
|
||||
variable_selector: [],
|
||||
},
|
||||
})
|
||||
}
|
||||
}, [getIsVisionModel, handleVisionResolutionEnabledChange, onChange, payload.enabled])
|
||||
|
||||
return {
|
||||
isVisionModel,
|
||||
handleVisionResolutionEnabledChange,
|
||||
handleVisionResolutionChange,
|
||||
handleModelChanged,
|
||||
}
|
||||
}
|
||||
|
||||
export default useConfigVision
|
||||
@@ -0,0 +1,144 @@
|
||||
import { useMemo } from 'react'
|
||||
import useNodes from '@/app/components/workflow/store/workflow/use-nodes'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { BlockEnum, type CommonNodeType } from '../types'
|
||||
import { getWorkflowEntryNode } from '../utils/workflow-entry'
|
||||
import { type TestRunOptions, type TriggerOption, TriggerType } from '../header/test-run-menu'
|
||||
import { TriggerAll } from '@/app/components/base/icons/src/vender/workflow'
|
||||
import BlockIcon from '../block-icon'
|
||||
import { useStore } from '../store'
|
||||
import { useAllTriggerPlugins } from '@/service/use-triggers'
|
||||
|
||||
export const useDynamicTestRunOptions = (): TestRunOptions => {
|
||||
const { t } = useTranslation()
|
||||
const nodes = useNodes()
|
||||
const buildInTools = useStore(s => s.buildInTools)
|
||||
const customTools = useStore(s => s.customTools)
|
||||
const workflowTools = useStore(s => s.workflowTools)
|
||||
const mcpTools = useStore(s => s.mcpTools)
|
||||
const { data: triggerPlugins } = useAllTriggerPlugins()
|
||||
|
||||
return useMemo(() => {
|
||||
const allTriggers: TriggerOption[] = []
|
||||
let userInput: TriggerOption | undefined
|
||||
|
||||
for (const node of nodes) {
|
||||
const nodeData = node.data as CommonNodeType
|
||||
|
||||
if (!nodeData?.type) continue
|
||||
|
||||
if (nodeData.type === BlockEnum.Start) {
|
||||
userInput = {
|
||||
id: node.id,
|
||||
type: TriggerType.UserInput,
|
||||
name: nodeData.title || t('workflow.blocks.start'),
|
||||
icon: (
|
||||
<BlockIcon
|
||||
type={BlockEnum.Start}
|
||||
size='md'
|
||||
/>
|
||||
),
|
||||
nodeId: node.id,
|
||||
enabled: true,
|
||||
}
|
||||
}
|
||||
else if (nodeData.type === BlockEnum.TriggerSchedule) {
|
||||
allTriggers.push({
|
||||
id: node.id,
|
||||
type: TriggerType.Schedule,
|
||||
name: nodeData.title || t('workflow.blocks.trigger-schedule'),
|
||||
icon: (
|
||||
<BlockIcon
|
||||
type={BlockEnum.TriggerSchedule}
|
||||
size='md'
|
||||
/>
|
||||
),
|
||||
nodeId: node.id,
|
||||
enabled: true,
|
||||
})
|
||||
}
|
||||
else if (nodeData.type === BlockEnum.TriggerWebhook) {
|
||||
allTriggers.push({
|
||||
id: node.id,
|
||||
type: TriggerType.Webhook,
|
||||
name: nodeData.title || t('workflow.blocks.trigger-webhook'),
|
||||
icon: (
|
||||
<BlockIcon
|
||||
type={BlockEnum.TriggerWebhook}
|
||||
size='md'
|
||||
/>
|
||||
),
|
||||
nodeId: node.id,
|
||||
enabled: true,
|
||||
})
|
||||
}
|
||||
else if (nodeData.type === BlockEnum.TriggerPlugin) {
|
||||
let triggerIcon: string | any
|
||||
|
||||
if (nodeData.provider_id) {
|
||||
const targetTriggers = triggerPlugins || []
|
||||
triggerIcon = targetTriggers.find(toolWithProvider => toolWithProvider.name === nodeData.provider_id)?.icon
|
||||
}
|
||||
|
||||
const icon = (
|
||||
<BlockIcon
|
||||
type={BlockEnum.TriggerPlugin}
|
||||
size='md'
|
||||
toolIcon={triggerIcon}
|
||||
/>
|
||||
)
|
||||
|
||||
allTriggers.push({
|
||||
id: node.id,
|
||||
type: TriggerType.Plugin,
|
||||
name: nodeData.title || (nodeData as any).plugin_name || t('workflow.blocks.trigger-plugin'),
|
||||
icon,
|
||||
nodeId: node.id,
|
||||
enabled: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (!userInput) {
|
||||
const startNode = getWorkflowEntryNode(nodes as any[])
|
||||
if (startNode && startNode.data?.type === BlockEnum.Start) {
|
||||
userInput = {
|
||||
id: startNode.id,
|
||||
type: TriggerType.UserInput,
|
||||
name: (startNode.data as CommonNodeType)?.title || t('workflow.blocks.start'),
|
||||
icon: (
|
||||
<BlockIcon
|
||||
type={BlockEnum.Start}
|
||||
size='md'
|
||||
/>
|
||||
),
|
||||
nodeId: startNode.id,
|
||||
enabled: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const triggerNodeIds = allTriggers
|
||||
.map(trigger => trigger.nodeId)
|
||||
.filter((nodeId): nodeId is string => Boolean(nodeId))
|
||||
|
||||
const runAll: TriggerOption | undefined = triggerNodeIds.length > 1 ? {
|
||||
id: 'run-all',
|
||||
type: TriggerType.All,
|
||||
name: t('workflow.common.runAllTriggers'),
|
||||
icon: (
|
||||
<div className="flex h-6 w-6 items-center justify-center rounded-lg border-[0.5px] border-white/2 bg-util-colors-purple-purple-500 text-white shadow-md">
|
||||
<TriggerAll className="h-4.5 w-4.5" />
|
||||
</div>
|
||||
),
|
||||
relatedNodeIds: triggerNodeIds,
|
||||
enabled: true,
|
||||
} : undefined
|
||||
|
||||
return {
|
||||
userInput,
|
||||
triggers: allTriggers,
|
||||
runAll,
|
||||
}
|
||||
}, [nodes, buildInTools, customTools, workflowTools, mcpTools, triggerPlugins, t])
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { useCallback } from 'react'
|
||||
import { produce } from 'immer'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
|
||||
export const useEdgesInteractionsWithoutSync = () => {
|
||||
const store = useStoreApi()
|
||||
|
||||
const handleEdgeCancelRunningStatus = useCallback(() => {
|
||||
const {
|
||||
edges,
|
||||
setEdges,
|
||||
} = store.getState()
|
||||
|
||||
const newEdges = produce(edges, (draft) => {
|
||||
draft.forEach((edge) => {
|
||||
edge.data._sourceRunningStatus = undefined
|
||||
edge.data._targetRunningStatus = undefined
|
||||
edge.data._waitingRun = false
|
||||
})
|
||||
})
|
||||
setEdges(newEdges)
|
||||
}, [store])
|
||||
|
||||
return {
|
||||
handleEdgeCancelRunningStatus,
|
||||
}
|
||||
}
|
||||
161
dify/web/app/components/workflow/hooks/use-edges-interactions.ts
Normal file
161
dify/web/app/components/workflow/hooks/use-edges-interactions.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import { useCallback } from 'react'
|
||||
import { produce } from 'immer'
|
||||
import type {
|
||||
EdgeMouseHandler,
|
||||
OnEdgesChange,
|
||||
} from 'reactflow'
|
||||
import {
|
||||
useStoreApi,
|
||||
} from 'reactflow'
|
||||
import type {
|
||||
Node,
|
||||
} from '../types'
|
||||
import { getNodesConnectedSourceOrTargetHandleIdsMap } from '../utils'
|
||||
import { useNodesSyncDraft } from './use-nodes-sync-draft'
|
||||
import { useNodesReadOnly } from './use-workflow'
|
||||
import { WorkflowHistoryEvent, useWorkflowHistory } from './use-workflow-history'
|
||||
|
||||
export const useEdgesInteractions = () => {
|
||||
const store = useStoreApi()
|
||||
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
const { getNodesReadOnly } = useNodesReadOnly()
|
||||
const { saveStateToHistory } = useWorkflowHistory()
|
||||
|
||||
const handleEdgeEnter = useCallback<EdgeMouseHandler>((_, edge) => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
const {
|
||||
edges,
|
||||
setEdges,
|
||||
} = store.getState()
|
||||
const newEdges = produce(edges, (draft) => {
|
||||
const currentEdge = draft.find(e => e.id === edge.id)!
|
||||
|
||||
currentEdge.data._hovering = true
|
||||
})
|
||||
setEdges(newEdges)
|
||||
}, [store, getNodesReadOnly])
|
||||
|
||||
const handleEdgeLeave = useCallback<EdgeMouseHandler>((_, edge) => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
const {
|
||||
edges,
|
||||
setEdges,
|
||||
} = store.getState()
|
||||
const newEdges = produce(edges, (draft) => {
|
||||
const currentEdge = draft.find(e => e.id === edge.id)!
|
||||
|
||||
currentEdge.data._hovering = false
|
||||
})
|
||||
setEdges(newEdges)
|
||||
}, [store, getNodesReadOnly])
|
||||
|
||||
const handleEdgeDeleteByDeleteBranch = useCallback((nodeId: string, branchId: string) => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
edges,
|
||||
setEdges,
|
||||
} = store.getState()
|
||||
const edgeWillBeDeleted = edges.filter(edge => edge.source === nodeId && edge.sourceHandle === branchId)
|
||||
|
||||
if (!edgeWillBeDeleted.length)
|
||||
return
|
||||
|
||||
const nodes = getNodes()
|
||||
const nodesConnectedSourceOrTargetHandleIdsMap = getNodesConnectedSourceOrTargetHandleIdsMap(
|
||||
edgeWillBeDeleted.map(edge => ({ type: 'remove', edge })),
|
||||
nodes,
|
||||
)
|
||||
const newNodes = produce(nodes, (draft: Node[]) => {
|
||||
draft.forEach((node) => {
|
||||
if (nodesConnectedSourceOrTargetHandleIdsMap[node.id]) {
|
||||
node.data = {
|
||||
...node.data,
|
||||
...nodesConnectedSourceOrTargetHandleIdsMap[node.id],
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
setNodes(newNodes)
|
||||
const newEdges = produce(edges, (draft) => {
|
||||
return draft.filter(edge => !edgeWillBeDeleted.find(e => e.id === edge.id))
|
||||
})
|
||||
setEdges(newEdges)
|
||||
handleSyncWorkflowDraft()
|
||||
saveStateToHistory(WorkflowHistoryEvent.EdgeDeleteByDeleteBranch)
|
||||
}, [getNodesReadOnly, store, handleSyncWorkflowDraft, saveStateToHistory])
|
||||
|
||||
const handleEdgeDelete = useCallback(() => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
edges,
|
||||
setEdges,
|
||||
} = store.getState()
|
||||
const currentEdgeIndex = edges.findIndex(edge => edge.selected)
|
||||
|
||||
if (currentEdgeIndex < 0)
|
||||
return
|
||||
const currentEdge = edges[currentEdgeIndex]
|
||||
const nodes = getNodes()
|
||||
const nodesConnectedSourceOrTargetHandleIdsMap = getNodesConnectedSourceOrTargetHandleIdsMap(
|
||||
[
|
||||
{ type: 'remove', edge: currentEdge },
|
||||
],
|
||||
nodes,
|
||||
)
|
||||
const newNodes = produce(nodes, (draft: Node[]) => {
|
||||
draft.forEach((node) => {
|
||||
if (nodesConnectedSourceOrTargetHandleIdsMap[node.id]) {
|
||||
node.data = {
|
||||
...node.data,
|
||||
...nodesConnectedSourceOrTargetHandleIdsMap[node.id],
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
setNodes(newNodes)
|
||||
const newEdges = produce(edges, (draft) => {
|
||||
draft.splice(currentEdgeIndex, 1)
|
||||
})
|
||||
setEdges(newEdges)
|
||||
handleSyncWorkflowDraft()
|
||||
saveStateToHistory(WorkflowHistoryEvent.EdgeDelete)
|
||||
}, [getNodesReadOnly, store, handleSyncWorkflowDraft, saveStateToHistory])
|
||||
|
||||
const handleEdgesChange = useCallback<OnEdgesChange>((changes) => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
const {
|
||||
edges,
|
||||
setEdges,
|
||||
} = store.getState()
|
||||
|
||||
const newEdges = produce(edges, (draft) => {
|
||||
changes.forEach((change) => {
|
||||
if (change.type === 'select')
|
||||
draft.find(edge => edge.id === change.id)!.selected = change.selected
|
||||
})
|
||||
})
|
||||
setEdges(newEdges)
|
||||
}, [store, getNodesReadOnly])
|
||||
|
||||
return {
|
||||
handleEdgeEnter,
|
||||
handleEdgeLeave,
|
||||
handleEdgeDeleteByDeleteBranch,
|
||||
handleEdgeDelete,
|
||||
handleEdgesChange,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import type { NodeWithVar, VarInInspect } from '@/types/workflow'
|
||||
import { useStore, useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import type { ToolWithProvider } from '@/app/components/workflow/types'
|
||||
import type { Node } from '@/app/components/workflow/types'
|
||||
import { fetchAllInspectVars } from '@/service/workflow'
|
||||
import { useInvalidateConversationVarValues, useInvalidateSysVarValues } from '@/service/use-workflow'
|
||||
import { useNodesInteractionsWithoutSync } from '@/app/components/workflow/hooks/use-nodes-interactions-without-sync'
|
||||
import type { FlowType } from '@/types/common'
|
||||
import useMatchSchemaType, { getMatchedSchemaType } from '../nodes/_base/components/variable/use-match-schema-type'
|
||||
import { toNodeOutputVars } from '../nodes/_base/components/variable/utils'
|
||||
import type { SchemaTypeDefinition } from '@/service/use-common'
|
||||
import { useCallback } from 'react'
|
||||
import {
|
||||
useAllBuiltInTools,
|
||||
useAllCustomTools,
|
||||
useAllMCPTools,
|
||||
useAllWorkflowTools,
|
||||
} from '@/service/use-tools'
|
||||
|
||||
type Params = {
|
||||
flowType: FlowType
|
||||
flowId: string
|
||||
}
|
||||
|
||||
export const useSetWorkflowVarsWithValue = ({
|
||||
flowType,
|
||||
flowId,
|
||||
}: Params) => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const store = useStoreApi()
|
||||
const invalidateConversationVarValues = useInvalidateConversationVarValues(flowType, flowId)
|
||||
const invalidateSysVarValues = useInvalidateSysVarValues(flowType, flowId)
|
||||
const { handleCancelAllNodeSuccessStatus } = useNodesInteractionsWithoutSync()
|
||||
const { schemaTypeDefinitions } = useMatchSchemaType()
|
||||
const { data: buildInTools } = useAllBuiltInTools()
|
||||
const { data: customTools } = useAllCustomTools()
|
||||
const { data: workflowTools } = useAllWorkflowTools()
|
||||
const { data: mcpTools } = useAllMCPTools()
|
||||
const dataSourceList = useStore(s => s.dataSourceList)
|
||||
const allPluginInfoList = {
|
||||
buildInTools: buildInTools || [],
|
||||
customTools: customTools || [],
|
||||
workflowTools: workflowTools || [],
|
||||
mcpTools: mcpTools || [],
|
||||
dataSourceList: dataSourceList || [],
|
||||
}
|
||||
|
||||
const setInspectVarsToStore = (inspectVars: VarInInspect[], passedInAllPluginInfoList?: Record<string, ToolWithProvider[]>, passedInSchemaTypeDefinitions?: SchemaTypeDefinition[]) => {
|
||||
const { setNodesWithInspectVars } = workflowStore.getState()
|
||||
const { getNodes } = store.getState()
|
||||
|
||||
const nodeArr = getNodes()
|
||||
const allNodesOutputVars = toNodeOutputVars(nodeArr, false, () => true, [], [], [], passedInAllPluginInfoList || allPluginInfoList, passedInSchemaTypeDefinitions || schemaTypeDefinitions)
|
||||
|
||||
const nodesKeyValue: Record<string, Node> = {}
|
||||
nodeArr.forEach((node) => {
|
||||
nodesKeyValue[node.id] = node
|
||||
})
|
||||
|
||||
const withValueNodeIds: Record<string, boolean> = {}
|
||||
inspectVars.forEach((varItem) => {
|
||||
const nodeId = varItem.selector[0]
|
||||
|
||||
const node = nodesKeyValue[nodeId]
|
||||
if (!node)
|
||||
return
|
||||
withValueNodeIds[nodeId] = true
|
||||
})
|
||||
const withValueNodes = Object.keys(withValueNodeIds).map((nodeId) => {
|
||||
return nodesKeyValue[nodeId]
|
||||
})
|
||||
|
||||
const res: NodeWithVar[] = withValueNodes.map((node) => {
|
||||
const nodeId = node.id
|
||||
const varsUnderTheNode = inspectVars.filter((varItem) => {
|
||||
return varItem.selector[0] === nodeId
|
||||
})
|
||||
const nodeVar = allNodesOutputVars.find(item => item.nodeId === nodeId)
|
||||
|
||||
const nodeWithVar = {
|
||||
nodeId,
|
||||
nodePayload: node.data,
|
||||
nodeType: node.data.type,
|
||||
title: node.data.title,
|
||||
vars: varsUnderTheNode.map((item) => {
|
||||
const schemaType = nodeVar ? nodeVar.vars.find(v => v.variable === item.name)?.schemaType : ''
|
||||
return {
|
||||
...item,
|
||||
schemaType,
|
||||
}
|
||||
}),
|
||||
isSingRunRunning: false,
|
||||
isValueFetched: false,
|
||||
}
|
||||
return nodeWithVar
|
||||
})
|
||||
setNodesWithInspectVars(res)
|
||||
}
|
||||
|
||||
const fetchInspectVars = useCallback(async (params: {
|
||||
passInVars?: boolean,
|
||||
vars?: VarInInspect[],
|
||||
passedInAllPluginInfoList?: Record<string, ToolWithProvider[]>,
|
||||
passedInSchemaTypeDefinitions?: SchemaTypeDefinition[]
|
||||
}) => {
|
||||
const { passInVars, vars, passedInAllPluginInfoList, passedInSchemaTypeDefinitions } = params
|
||||
invalidateConversationVarValues()
|
||||
invalidateSysVarValues()
|
||||
const data = passInVars ? vars! : await fetchAllInspectVars(flowType, flowId)
|
||||
setInspectVarsToStore(data, passedInAllPluginInfoList, passedInSchemaTypeDefinitions)
|
||||
handleCancelAllNodeSuccessStatus() // to make sure clear node output show the unset status
|
||||
}, [invalidateConversationVarValues, invalidateSysVarValues, flowType, flowId, setInspectVarsToStore, handleCancelAllNodeSuccessStatus, schemaTypeDefinitions, getMatchedSchemaType])
|
||||
return {
|
||||
fetchInspectVars,
|
||||
}
|
||||
}
|
||||
196
dify/web/app/components/workflow/hooks/use-helpline.ts
Normal file
196
dify/web/app/components/workflow/hooks/use-helpline.ts
Normal file
@@ -0,0 +1,196 @@
|
||||
import { useCallback } from 'react'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import type { Node } from '../types'
|
||||
import { BlockEnum, isTriggerNode } from '../types'
|
||||
import { useWorkflowStore } from '../store'
|
||||
|
||||
// Entry node (Start/Trigger) wrapper offsets
|
||||
// The EntryNodeContainer adds a wrapper with status indicator above the actual node
|
||||
// These offsets ensure alignment happens on the inner node, not the wrapper
|
||||
const ENTRY_NODE_WRAPPER_OFFSET = {
|
||||
x: 0, // No horizontal padding on wrapper (px-0)
|
||||
y: 21, // Actual measured: pt-0.5 (2px) + status bar height (~19px)
|
||||
} as const
|
||||
|
||||
export const useHelpline = () => {
|
||||
const store = useStoreApi()
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
// Check if a node is an entry node (Start or Trigger)
|
||||
const isEntryNode = useCallback((node: Node): boolean => {
|
||||
return isTriggerNode(node.data.type as any) || node.data.type === BlockEnum.Start
|
||||
}, [])
|
||||
|
||||
// Get the actual alignment position of a node (accounting for wrapper offset)
|
||||
const getNodeAlignPosition = useCallback((node: Node) => {
|
||||
if (isEntryNode(node)) {
|
||||
return {
|
||||
x: node.position.x + ENTRY_NODE_WRAPPER_OFFSET.x,
|
||||
y: node.position.y + ENTRY_NODE_WRAPPER_OFFSET.y,
|
||||
}
|
||||
}
|
||||
return {
|
||||
x: node.position.x,
|
||||
y: node.position.y,
|
||||
}
|
||||
}, [isEntryNode])
|
||||
|
||||
const handleSetHelpline = useCallback((node: Node) => {
|
||||
const { getNodes } = store.getState()
|
||||
const nodes = getNodes()
|
||||
const {
|
||||
setHelpLineHorizontal,
|
||||
setHelpLineVertical,
|
||||
} = workflowStore.getState()
|
||||
|
||||
if (node.data.isInIteration) {
|
||||
return {
|
||||
showHorizontalHelpLineNodes: [],
|
||||
showVerticalHelpLineNodes: [],
|
||||
}
|
||||
}
|
||||
|
||||
if (node.data.isInLoop) {
|
||||
return {
|
||||
showHorizontalHelpLineNodes: [],
|
||||
showVerticalHelpLineNodes: [],
|
||||
}
|
||||
}
|
||||
|
||||
// Get the actual alignment position for the dragging node
|
||||
const nodeAlignPos = getNodeAlignPosition(node)
|
||||
|
||||
const showHorizontalHelpLineNodes = nodes.filter((n) => {
|
||||
if (n.id === node.id)
|
||||
return false
|
||||
|
||||
if (n.data.isInIteration)
|
||||
return false
|
||||
|
||||
if (n.data.isInLoop)
|
||||
return false
|
||||
|
||||
// Get actual alignment position for comparison node
|
||||
const nAlignPos = getNodeAlignPosition(n)
|
||||
const nY = Math.ceil(nAlignPos.y)
|
||||
const nodeY = Math.ceil(nodeAlignPos.y)
|
||||
|
||||
if (nY - nodeY < 5 && nY - nodeY > -5)
|
||||
return true
|
||||
|
||||
return false
|
||||
}).sort((a, b) => {
|
||||
const aPos = getNodeAlignPosition(a)
|
||||
const bPos = getNodeAlignPosition(b)
|
||||
return aPos.x - bPos.x
|
||||
})
|
||||
|
||||
const showHorizontalHelpLineNodesLength = showHorizontalHelpLineNodes.length
|
||||
if (showHorizontalHelpLineNodesLength > 0) {
|
||||
const first = showHorizontalHelpLineNodes[0]
|
||||
const last = showHorizontalHelpLineNodes[showHorizontalHelpLineNodesLength - 1]
|
||||
|
||||
// Use actual alignment positions for help line rendering
|
||||
const firstPos = getNodeAlignPosition(first)
|
||||
const lastPos = getNodeAlignPosition(last)
|
||||
|
||||
// For entry nodes, we need to subtract the offset from width since lastPos already includes it
|
||||
const lastIsEntryNode = isEntryNode(last)
|
||||
const lastNodeWidth = lastIsEntryNode ? last.width! - ENTRY_NODE_WRAPPER_OFFSET.x : last.width!
|
||||
|
||||
const helpLine = {
|
||||
top: firstPos.y,
|
||||
left: firstPos.x,
|
||||
width: lastPos.x + lastNodeWidth - firstPos.x,
|
||||
}
|
||||
|
||||
if (nodeAlignPos.x < firstPos.x) {
|
||||
const firstIsEntryNode = isEntryNode(first)
|
||||
const firstNodeWidth = firstIsEntryNode ? first.width! - ENTRY_NODE_WRAPPER_OFFSET.x : first.width!
|
||||
helpLine.left = nodeAlignPos.x
|
||||
helpLine.width = firstPos.x + firstNodeWidth - nodeAlignPos.x
|
||||
}
|
||||
|
||||
if (nodeAlignPos.x > lastPos.x) {
|
||||
const nodeIsEntryNode = isEntryNode(node)
|
||||
const nodeWidth = nodeIsEntryNode ? node.width! - ENTRY_NODE_WRAPPER_OFFSET.x : node.width!
|
||||
helpLine.width = nodeAlignPos.x + nodeWidth - firstPos.x
|
||||
}
|
||||
|
||||
setHelpLineHorizontal(helpLine)
|
||||
}
|
||||
else {
|
||||
setHelpLineHorizontal()
|
||||
}
|
||||
|
||||
const showVerticalHelpLineNodes = nodes.filter((n) => {
|
||||
if (n.id === node.id)
|
||||
return false
|
||||
if (n.data.isInIteration)
|
||||
return false
|
||||
if (n.data.isInLoop)
|
||||
return false
|
||||
|
||||
// Get actual alignment position for comparison node
|
||||
const nAlignPos = getNodeAlignPosition(n)
|
||||
const nX = Math.ceil(nAlignPos.x)
|
||||
const nodeX = Math.ceil(nodeAlignPos.x)
|
||||
|
||||
if (nX - nodeX < 5 && nX - nodeX > -5)
|
||||
return true
|
||||
|
||||
return false
|
||||
}).sort((a, b) => {
|
||||
const aPos = getNodeAlignPosition(a)
|
||||
const bPos = getNodeAlignPosition(b)
|
||||
return aPos.x - bPos.x
|
||||
})
|
||||
const showVerticalHelpLineNodesLength = showVerticalHelpLineNodes.length
|
||||
|
||||
if (showVerticalHelpLineNodesLength > 0) {
|
||||
const first = showVerticalHelpLineNodes[0]
|
||||
const last = showVerticalHelpLineNodes[showVerticalHelpLineNodesLength - 1]
|
||||
|
||||
// Use actual alignment positions for help line rendering
|
||||
const firstPos = getNodeAlignPosition(first)
|
||||
const lastPos = getNodeAlignPosition(last)
|
||||
|
||||
// For entry nodes, we need to subtract the offset from height since lastPos already includes it
|
||||
const lastIsEntryNode = isEntryNode(last)
|
||||
const lastNodeHeight = lastIsEntryNode ? last.height! - ENTRY_NODE_WRAPPER_OFFSET.y : last.height!
|
||||
|
||||
const helpLine = {
|
||||
top: firstPos.y,
|
||||
left: firstPos.x,
|
||||
height: lastPos.y + lastNodeHeight - firstPos.y,
|
||||
}
|
||||
|
||||
if (nodeAlignPos.y < firstPos.y) {
|
||||
const firstIsEntryNode = isEntryNode(first)
|
||||
const firstNodeHeight = firstIsEntryNode ? first.height! - ENTRY_NODE_WRAPPER_OFFSET.y : first.height!
|
||||
helpLine.top = nodeAlignPos.y
|
||||
helpLine.height = firstPos.y + firstNodeHeight - nodeAlignPos.y
|
||||
}
|
||||
|
||||
if (nodeAlignPos.y > lastPos.y) {
|
||||
const nodeIsEntryNode = isEntryNode(node)
|
||||
const nodeHeight = nodeIsEntryNode ? node.height! - ENTRY_NODE_WRAPPER_OFFSET.y : node.height!
|
||||
helpLine.height = nodeAlignPos.y + nodeHeight - firstPos.y
|
||||
}
|
||||
|
||||
setHelpLineVertical(helpLine)
|
||||
}
|
||||
else {
|
||||
setHelpLineVertical()
|
||||
}
|
||||
|
||||
return {
|
||||
showHorizontalHelpLineNodes,
|
||||
showVerticalHelpLineNodes,
|
||||
}
|
||||
}, [store, workflowStore, getNodeAlignPosition])
|
||||
|
||||
return {
|
||||
handleSetHelpline,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
import { fetchNodeInspectVars } from '@/service/workflow'
|
||||
import { useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import type { ValueSelector } from '@/app/components/workflow/types'
|
||||
import type { VarInInspect } from '@/types/workflow'
|
||||
import { VarInInspectType } from '@/types/workflow'
|
||||
import { useCallback } from 'react'
|
||||
import {
|
||||
isConversationVar,
|
||||
isENV,
|
||||
isSystemVar,
|
||||
toNodeOutputVars,
|
||||
} from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
||||
import { produce } from 'immer'
|
||||
import type { Node } from '@/app/components/workflow/types'
|
||||
import { useNodesInteractionsWithoutSync } from '@/app/components/workflow/hooks/use-nodes-interactions-without-sync'
|
||||
import { useEdgesInteractionsWithoutSync } from '@/app/components/workflow/hooks/use-edges-interactions-without-sync'
|
||||
import type { FlowType } from '@/types/common'
|
||||
import useFLow from '@/service/use-flow'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import type { SchemaTypeDefinition } from '@/service/use-common'
|
||||
import {
|
||||
useAllBuiltInTools,
|
||||
useAllCustomTools,
|
||||
useAllMCPTools,
|
||||
useAllWorkflowTools,
|
||||
} from '@/service/use-tools'
|
||||
|
||||
type Params = {
|
||||
flowId: string
|
||||
flowType: FlowType
|
||||
}
|
||||
export const useInspectVarsCrudCommon = ({
|
||||
flowId,
|
||||
flowType,
|
||||
}: Params) => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const store = useStoreApi()
|
||||
const {
|
||||
useInvalidateConversationVarValues,
|
||||
useInvalidateSysVarValues,
|
||||
useResetConversationVar,
|
||||
useResetToLastRunValue,
|
||||
useDeleteAllInspectorVars,
|
||||
useDeleteNodeInspectorVars,
|
||||
useDeleteInspectVar,
|
||||
useEditInspectorVar,
|
||||
} = useFLow({ flowType })
|
||||
const invalidateConversationVarValues = useInvalidateConversationVarValues(flowId)
|
||||
const { mutateAsync: doResetConversationVar } = useResetConversationVar(flowId)
|
||||
const { mutateAsync: doResetToLastRunValue } = useResetToLastRunValue(flowId)
|
||||
const invalidateSysVarValues = useInvalidateSysVarValues(flowId)
|
||||
|
||||
const { mutateAsync: doDeleteAllInspectorVars } = useDeleteAllInspectorVars(flowId)
|
||||
const { mutate: doDeleteNodeInspectorVars } = useDeleteNodeInspectorVars(flowId)
|
||||
const { mutate: doDeleteInspectVar } = useDeleteInspectVar(flowId)
|
||||
|
||||
const { mutateAsync: doEditInspectorVar } = useEditInspectorVar(flowId)
|
||||
const { handleCancelNodeSuccessStatus } = useNodesInteractionsWithoutSync()
|
||||
const { handleEdgeCancelRunningStatus } = useEdgesInteractionsWithoutSync()
|
||||
const { data: buildInTools } = useAllBuiltInTools()
|
||||
const { data: customTools } = useAllCustomTools()
|
||||
const { data: workflowTools } = useAllWorkflowTools()
|
||||
const { data: mcpTools } = useAllMCPTools()
|
||||
|
||||
const getNodeInspectVars = useCallback((nodeId: string) => {
|
||||
const { nodesWithInspectVars } = workflowStore.getState()
|
||||
const node = nodesWithInspectVars.find(node => node.nodeId === nodeId)
|
||||
return node
|
||||
}, [workflowStore])
|
||||
|
||||
const getVarId = useCallback((nodeId: string, varName: string) => {
|
||||
const node = getNodeInspectVars(nodeId)
|
||||
if (!node)
|
||||
return undefined
|
||||
const varId = node.vars.find((varItem) => {
|
||||
return varItem.selector[1] === varName
|
||||
})?.id
|
||||
return varId
|
||||
}, [getNodeInspectVars])
|
||||
|
||||
const getInspectVar = useCallback((nodeId: string, name: string): VarInInspect | undefined => {
|
||||
const node = getNodeInspectVars(nodeId)
|
||||
if (!node)
|
||||
return undefined
|
||||
|
||||
const variable = node.vars.find((varItem) => {
|
||||
return varItem.name === name
|
||||
})
|
||||
return variable
|
||||
}, [getNodeInspectVars])
|
||||
|
||||
const hasSetInspectVar = useCallback((nodeId: string, name: string, sysVars: VarInInspect[], conversationVars: VarInInspect[]) => {
|
||||
const isEnv = isENV([nodeId])
|
||||
if (isEnv) // always have value
|
||||
return true
|
||||
const isSys = isSystemVar([nodeId])
|
||||
if (isSys)
|
||||
return sysVars.some(varItem => varItem.selector?.[1]?.replace('sys.', '') === name)
|
||||
const isChatVar = isConversationVar([nodeId])
|
||||
if (isChatVar)
|
||||
return conversationVars.some(varItem => varItem.selector?.[1] === name)
|
||||
return getInspectVar(nodeId, name) !== undefined
|
||||
}, [getInspectVar])
|
||||
|
||||
const hasNodeInspectVars = useCallback((nodeId: string) => {
|
||||
return !!getNodeInspectVars(nodeId)
|
||||
}, [getNodeInspectVars])
|
||||
|
||||
const fetchInspectVarValue = useCallback(async (selector: ValueSelector, schemaTypeDefinitions: SchemaTypeDefinition[]) => {
|
||||
const {
|
||||
setNodeInspectVars,
|
||||
dataSourceList,
|
||||
} = workflowStore.getState()
|
||||
const nodeId = selector[0]
|
||||
const isSystemVar = nodeId === 'sys'
|
||||
const isConversationVar = nodeId === 'conversation'
|
||||
if (isSystemVar) {
|
||||
invalidateSysVarValues()
|
||||
return
|
||||
}
|
||||
if (isConversationVar) {
|
||||
invalidateConversationVarValues()
|
||||
return
|
||||
}
|
||||
const { getNodes } = store.getState()
|
||||
const nodeArr = getNodes()
|
||||
const currentNode = nodeArr.find(node => node.id === nodeId)
|
||||
const allPluginInfoList = {
|
||||
buildInTools: buildInTools || [],
|
||||
customTools: customTools || [],
|
||||
workflowTools: workflowTools || [],
|
||||
mcpTools: mcpTools || [],
|
||||
dataSourceList: dataSourceList || [],
|
||||
}
|
||||
const currentNodeOutputVars = toNodeOutputVars([currentNode], false, () => true, [], [], [], allPluginInfoList, schemaTypeDefinitions)
|
||||
const vars = await fetchNodeInspectVars(flowType, flowId, nodeId)
|
||||
const varsWithSchemaType = vars.map((varItem) => {
|
||||
const schemaType = currentNodeOutputVars[0]?.vars.find(v => v.variable === varItem.name)?.schemaType || ''
|
||||
return {
|
||||
...varItem,
|
||||
schemaType,
|
||||
}
|
||||
})
|
||||
setNodeInspectVars(nodeId, varsWithSchemaType)
|
||||
}, [workflowStore, flowType, flowId, invalidateSysVarValues, invalidateConversationVarValues, buildInTools, customTools, workflowTools, mcpTools])
|
||||
|
||||
// after last run would call this
|
||||
const appendNodeInspectVars = useCallback((nodeId: string, payload: VarInInspect[], allNodes: Node[]) => {
|
||||
const {
|
||||
nodesWithInspectVars,
|
||||
setNodesWithInspectVars,
|
||||
} = workflowStore.getState()
|
||||
const nodes = produce(nodesWithInspectVars, (draft) => {
|
||||
const nodeInfo = allNodes.find(node => node.id === nodeId)
|
||||
if (nodeInfo) {
|
||||
const index = draft.findIndex(node => node.nodeId === nodeId)
|
||||
if (index === -1) {
|
||||
draft.unshift({
|
||||
nodeId,
|
||||
nodeType: nodeInfo.data.type,
|
||||
title: nodeInfo.data.title,
|
||||
vars: payload,
|
||||
nodePayload: nodeInfo.data,
|
||||
})
|
||||
}
|
||||
else {
|
||||
draft[index].vars = payload
|
||||
// put the node to the topAdd commentMore actions
|
||||
draft.unshift(draft.splice(index, 1)[0])
|
||||
}
|
||||
}
|
||||
})
|
||||
setNodesWithInspectVars(nodes)
|
||||
handleCancelNodeSuccessStatus(nodeId)
|
||||
}, [workflowStore, handleCancelNodeSuccessStatus])
|
||||
|
||||
const hasNodeInspectVar = useCallback((nodeId: string, varId: string) => {
|
||||
const { nodesWithInspectVars } = workflowStore.getState()
|
||||
const targetNode = nodesWithInspectVars.find(item => item.nodeId === nodeId)
|
||||
if (!targetNode || !targetNode.vars)
|
||||
return false
|
||||
return targetNode.vars.some(item => item.id === varId)
|
||||
}, [workflowStore])
|
||||
|
||||
const deleteInspectVar = useCallback(async (nodeId: string, varId: string) => {
|
||||
const { deleteInspectVar } = workflowStore.getState()
|
||||
if (hasNodeInspectVar(nodeId, varId)) {
|
||||
await doDeleteInspectVar(varId)
|
||||
deleteInspectVar(nodeId, varId)
|
||||
}
|
||||
}, [doDeleteInspectVar, workflowStore, hasNodeInspectVar])
|
||||
|
||||
const resetConversationVar = useCallback(async (varId: string) => {
|
||||
await doResetConversationVar(varId)
|
||||
invalidateConversationVarValues()
|
||||
}, [doResetConversationVar, invalidateConversationVarValues])
|
||||
|
||||
const deleteNodeInspectorVars = useCallback(async (nodeId: string) => {
|
||||
const { deleteNodeInspectVars } = workflowStore.getState()
|
||||
if (hasNodeInspectVars(nodeId)) {
|
||||
await doDeleteNodeInspectorVars(nodeId)
|
||||
deleteNodeInspectVars(nodeId)
|
||||
}
|
||||
}, [doDeleteNodeInspectorVars, workflowStore, hasNodeInspectVars])
|
||||
|
||||
const deleteAllInspectorVars = useCallback(async () => {
|
||||
const { deleteAllInspectVars } = workflowStore.getState()
|
||||
await doDeleteAllInspectorVars()
|
||||
await invalidateConversationVarValues()
|
||||
await invalidateSysVarValues()
|
||||
deleteAllInspectVars()
|
||||
handleEdgeCancelRunningStatus()
|
||||
}, [doDeleteAllInspectorVars, invalidateConversationVarValues, invalidateSysVarValues, workflowStore, handleEdgeCancelRunningStatus])
|
||||
|
||||
const editInspectVarValue = useCallback(async (nodeId: string, varId: string, value: any) => {
|
||||
const { setInspectVarValue } = workflowStore.getState()
|
||||
await doEditInspectorVar({
|
||||
varId,
|
||||
value,
|
||||
})
|
||||
setInspectVarValue(nodeId, varId, value)
|
||||
if (nodeId === VarInInspectType.conversation)
|
||||
invalidateConversationVarValues()
|
||||
if (nodeId === VarInInspectType.system)
|
||||
invalidateSysVarValues()
|
||||
}, [doEditInspectorVar, invalidateConversationVarValues, invalidateSysVarValues, workflowStore])
|
||||
|
||||
const renameInspectVarName = useCallback(async (nodeId: string, oldName: string, newName: string) => {
|
||||
const { renameInspectVarName } = workflowStore.getState()
|
||||
const varId = getVarId(nodeId, oldName)
|
||||
if (!varId)
|
||||
return
|
||||
|
||||
const newSelector = [nodeId, newName]
|
||||
await doEditInspectorVar({
|
||||
varId,
|
||||
name: newName,
|
||||
})
|
||||
renameInspectVarName(nodeId, varId, newSelector)
|
||||
}, [doEditInspectorVar, getVarId, workflowStore])
|
||||
|
||||
const isInspectVarEdited = useCallback((nodeId: string, name: string) => {
|
||||
const inspectVar = getInspectVar(nodeId, name)
|
||||
if (!inspectVar)
|
||||
return false
|
||||
|
||||
return inspectVar.edited
|
||||
}, [getInspectVar])
|
||||
|
||||
const resetToLastRunVar = useCallback(async (nodeId: string, varId: string) => {
|
||||
const { resetToLastRunVar } = workflowStore.getState()
|
||||
const isSysVar = nodeId === 'sys'
|
||||
const data = await doResetToLastRunValue(varId)
|
||||
|
||||
if (isSysVar)
|
||||
invalidateSysVarValues()
|
||||
else
|
||||
resetToLastRunVar(nodeId, varId, data.value)
|
||||
}, [doResetToLastRunValue, invalidateSysVarValues, workflowStore])
|
||||
|
||||
return {
|
||||
hasNodeInspectVars,
|
||||
hasSetInspectVar,
|
||||
fetchInspectVarValue,
|
||||
editInspectVarValue,
|
||||
renameInspectVarName,
|
||||
appendNodeInspectVars,
|
||||
deleteInspectVar,
|
||||
deleteNodeInspectorVars,
|
||||
deleteAllInspectorVars,
|
||||
isInspectVarEdited,
|
||||
resetToLastRunVar,
|
||||
invalidateSysVarValues,
|
||||
resetConversationVar,
|
||||
invalidateConversationVarValues,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { useStore } from '../store'
|
||||
import { useHooksStore } from '@/app/components/workflow/hooks-store'
|
||||
import {
|
||||
useConversationVarValues,
|
||||
useSysVarValues,
|
||||
} from '@/service/use-workflow'
|
||||
import { FlowType } from '@/types/common'
|
||||
import { produce } from 'immer'
|
||||
import { BlockEnum } from '../types'
|
||||
|
||||
const varsAppendStartNodeKeys = ['query', 'files']
|
||||
const useInspectVarsCrud = () => {
|
||||
const partOfNodesWithInspectVars = useStore(s => s.nodesWithInspectVars)
|
||||
const configsMap = useHooksStore(s => s.configsMap)
|
||||
const isRagPipeline = configsMap?.flowType === FlowType.ragPipeline
|
||||
const { data: conversationVars } = useConversationVarValues(configsMap?.flowType, !isRagPipeline ? configsMap?.flowId : '')
|
||||
const { data: allSystemVars } = useSysVarValues(configsMap?.flowType, !isRagPipeline ? configsMap?.flowId : '')
|
||||
const { varsAppendStartNode, systemVars } = (() => {
|
||||
if(allSystemVars?.length === 0)
|
||||
return { varsAppendStartNode: [], systemVars: [] }
|
||||
const varsAppendStartNode = allSystemVars?.filter(({ name }) => varsAppendStartNodeKeys.includes(name)) || []
|
||||
const systemVars = allSystemVars?.filter(({ name }) => !varsAppendStartNodeKeys.includes(name)) || []
|
||||
return { varsAppendStartNode, systemVars }
|
||||
})()
|
||||
const nodesWithInspectVars = (() => {
|
||||
if(!partOfNodesWithInspectVars || partOfNodesWithInspectVars.length === 0)
|
||||
return []
|
||||
|
||||
const nodesWithInspectVars = produce(partOfNodesWithInspectVars, (draft) => {
|
||||
draft.forEach((nodeWithVars) => {
|
||||
if(nodeWithVars.nodeType === BlockEnum.Start)
|
||||
nodeWithVars.vars = [...nodeWithVars.vars, ...varsAppendStartNode]
|
||||
})
|
||||
})
|
||||
return nodesWithInspectVars
|
||||
})()
|
||||
const hasNodeInspectVars = useHooksStore(s => s.hasNodeInspectVars)
|
||||
const hasSetInspectVar = useHooksStore(s => s.hasSetInspectVar)
|
||||
const fetchInspectVarValue = useHooksStore(s => s.fetchInspectVarValue)
|
||||
const editInspectVarValue = useHooksStore(s => s.editInspectVarValue)
|
||||
const renameInspectVarName = useHooksStore(s => s.renameInspectVarName)
|
||||
const appendNodeInspectVars = useHooksStore(s => s.appendNodeInspectVars)
|
||||
const deleteInspectVar = useHooksStore(s => s.deleteInspectVar)
|
||||
const deleteNodeInspectorVars = useHooksStore(s => s.deleteNodeInspectorVars)
|
||||
const deleteAllInspectorVars = useHooksStore(s => s.deleteAllInspectorVars)
|
||||
const isInspectVarEdited = useHooksStore(s => s.isInspectVarEdited)
|
||||
const resetToLastRunVar = useHooksStore(s => s.resetToLastRunVar)
|
||||
const invalidateSysVarValues = useHooksStore(s => s.invalidateSysVarValues)
|
||||
const resetConversationVar = useHooksStore(s => s.resetConversationVar)
|
||||
const invalidateConversationVarValues = useHooksStore(s => s.invalidateConversationVarValues)
|
||||
|
||||
return {
|
||||
conversationVars: conversationVars || [],
|
||||
systemVars: systemVars || [],
|
||||
nodesWithInspectVars,
|
||||
hasNodeInspectVars,
|
||||
hasSetInspectVar,
|
||||
fetchInspectVarValue,
|
||||
editInspectVarValue,
|
||||
renameInspectVarName,
|
||||
appendNodeInspectVars,
|
||||
deleteInspectVar,
|
||||
deleteNodeInspectorVars,
|
||||
deleteAllInspectorVars,
|
||||
isInspectVarEdited,
|
||||
resetToLastRunVar,
|
||||
invalidateSysVarValues,
|
||||
resetConversationVar,
|
||||
invalidateConversationVarValues,
|
||||
}
|
||||
}
|
||||
|
||||
export default useInspectVarsCrud
|
||||
@@ -0,0 +1,51 @@
|
||||
import { useCallback } from 'react'
|
||||
import { produce } from 'immer'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import type { SyncCallback } from './use-nodes-sync-draft'
|
||||
import { useNodesSyncDraft } from './use-nodes-sync-draft'
|
||||
import { useNodesReadOnly } from './use-workflow'
|
||||
|
||||
type NodeDataUpdatePayload = {
|
||||
id: string
|
||||
data: Record<string, any>
|
||||
}
|
||||
|
||||
export const useNodeDataUpdate = () => {
|
||||
const store = useStoreApi()
|
||||
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
const { getNodesReadOnly } = useNodesReadOnly()
|
||||
|
||||
const handleNodeDataUpdate = useCallback(({ id, data }: NodeDataUpdatePayload) => {
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
} = store.getState()
|
||||
const newNodes = produce(getNodes(), (draft) => {
|
||||
const currentNode = draft.find(node => node.id === id)!
|
||||
|
||||
if (currentNode)
|
||||
currentNode.data = { ...currentNode.data, ...data }
|
||||
})
|
||||
setNodes(newNodes)
|
||||
}, [store])
|
||||
|
||||
const handleNodeDataUpdateWithSyncDraft = useCallback((
|
||||
payload: NodeDataUpdatePayload,
|
||||
options?: {
|
||||
sync?: boolean
|
||||
notRefreshWhenSyncError?: boolean
|
||||
callback?: SyncCallback
|
||||
},
|
||||
) => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
handleNodeDataUpdate(payload)
|
||||
handleSyncWorkflowDraft(options?.sync, options?.notRefreshWhenSyncError, options?.callback)
|
||||
}, [handleSyncWorkflowDraft, handleNodeDataUpdate, getNodesReadOnly])
|
||||
|
||||
return {
|
||||
handleNodeDataUpdate,
|
||||
handleNodeDataUpdateWithSyncDraft,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { BlockEnum, type CommonNodeType } from '../types'
|
||||
import type { ToolNodeType } from '../nodes/tool/types'
|
||||
import type { PluginTriggerNodeType } from '../nodes/trigger-plugin/types'
|
||||
import type { DataSourceNodeType } from '../nodes/data-source/types'
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
import {
|
||||
useAllBuiltInTools,
|
||||
useAllCustomTools,
|
||||
useAllMCPTools,
|
||||
useAllWorkflowTools,
|
||||
useInvalidToolsByType,
|
||||
} from '@/service/use-tools'
|
||||
import {
|
||||
useAllTriggerPlugins,
|
||||
useInvalidateAllTriggerPlugins,
|
||||
} from '@/service/use-triggers'
|
||||
import { useInvalidDataSourceList } from '@/service/use-pipeline'
|
||||
import { useStore } from '../store'
|
||||
import { canFindTool } from '@/utils'
|
||||
|
||||
type InstallationState = {
|
||||
isChecking: boolean
|
||||
isMissing: boolean
|
||||
uniqueIdentifier?: string
|
||||
canInstall: boolean
|
||||
onInstallSuccess: () => void
|
||||
shouldDim: boolean
|
||||
}
|
||||
|
||||
const useToolInstallation = (data: ToolNodeType): InstallationState => {
|
||||
const builtInQuery = useAllBuiltInTools()
|
||||
const customQuery = useAllCustomTools()
|
||||
const workflowQuery = useAllWorkflowTools()
|
||||
const mcpQuery = useAllMCPTools()
|
||||
const invalidateTools = useInvalidToolsByType(data.provider_type)
|
||||
|
||||
const collectionInfo = useMemo(() => {
|
||||
switch (data.provider_type) {
|
||||
case CollectionType.builtIn:
|
||||
return {
|
||||
list: builtInQuery.data,
|
||||
isLoading: builtInQuery.isLoading,
|
||||
}
|
||||
case CollectionType.custom:
|
||||
return {
|
||||
list: customQuery.data,
|
||||
isLoading: customQuery.isLoading,
|
||||
}
|
||||
case CollectionType.workflow:
|
||||
return {
|
||||
list: workflowQuery.data,
|
||||
isLoading: workflowQuery.isLoading,
|
||||
}
|
||||
case CollectionType.mcp:
|
||||
return {
|
||||
list: mcpQuery.data,
|
||||
isLoading: mcpQuery.isLoading,
|
||||
}
|
||||
default:
|
||||
return undefined
|
||||
}
|
||||
}, [
|
||||
builtInQuery.data,
|
||||
builtInQuery.isLoading,
|
||||
customQuery.data,
|
||||
customQuery.isLoading,
|
||||
data.provider_type,
|
||||
mcpQuery.data,
|
||||
mcpQuery.isLoading,
|
||||
workflowQuery.data,
|
||||
workflowQuery.isLoading,
|
||||
])
|
||||
|
||||
const collection = collectionInfo?.list
|
||||
const isLoading = collectionInfo?.isLoading ?? false
|
||||
const isResolved = !!collectionInfo && !isLoading
|
||||
|
||||
const matchedCollection = useMemo(() => {
|
||||
if (!collection || !collection.length)
|
||||
return undefined
|
||||
|
||||
return collection.find((toolWithProvider) => {
|
||||
if (data.plugin_id && toolWithProvider.plugin_id === data.plugin_id)
|
||||
return true
|
||||
if (canFindTool(toolWithProvider.id, data.provider_id))
|
||||
return true
|
||||
if (toolWithProvider.name === data.provider_name)
|
||||
return true
|
||||
return false
|
||||
})
|
||||
}, [collection, data.plugin_id, data.provider_id, data.provider_name])
|
||||
|
||||
const uniqueIdentifier = data.plugin_unique_identifier || data.plugin_id || data.provider_id
|
||||
const canInstall = Boolean(data.plugin_unique_identifier)
|
||||
|
||||
const onInstallSuccess = useCallback(() => {
|
||||
if (invalidateTools)
|
||||
invalidateTools()
|
||||
}, [invalidateTools])
|
||||
|
||||
const shouldDim = (!!collectionInfo && !isResolved) || (isResolved && !matchedCollection)
|
||||
|
||||
return {
|
||||
isChecking: !!collectionInfo && !isResolved,
|
||||
isMissing: isResolved && !matchedCollection,
|
||||
uniqueIdentifier,
|
||||
canInstall,
|
||||
onInstallSuccess,
|
||||
shouldDim,
|
||||
}
|
||||
}
|
||||
|
||||
const useTriggerInstallation = (data: PluginTriggerNodeType): InstallationState => {
|
||||
const triggerPluginsQuery = useAllTriggerPlugins()
|
||||
const invalidateTriggers = useInvalidateAllTriggerPlugins()
|
||||
|
||||
const triggerProviders = triggerPluginsQuery.data
|
||||
const isLoading = triggerPluginsQuery.isLoading
|
||||
|
||||
const matchedProvider = useMemo(() => {
|
||||
if (!triggerProviders || !triggerProviders.length)
|
||||
return undefined
|
||||
|
||||
return triggerProviders.find(provider =>
|
||||
provider.name === data.provider_name
|
||||
|| provider.id === data.provider_id
|
||||
|| (data.plugin_id && provider.plugin_id === data.plugin_id),
|
||||
)
|
||||
}, [
|
||||
data.plugin_id,
|
||||
data.provider_id,
|
||||
data.provider_name,
|
||||
triggerProviders,
|
||||
])
|
||||
|
||||
const uniqueIdentifier = data.plugin_unique_identifier || data.plugin_id || data.provider_id
|
||||
const canInstall = Boolean(data.plugin_unique_identifier)
|
||||
|
||||
const onInstallSuccess = useCallback(() => {
|
||||
invalidateTriggers()
|
||||
}, [invalidateTriggers])
|
||||
|
||||
const shouldDim = isLoading || (!isLoading && !!triggerProviders && !matchedProvider)
|
||||
|
||||
return {
|
||||
isChecking: isLoading,
|
||||
isMissing: !isLoading && !!triggerProviders && !matchedProvider,
|
||||
uniqueIdentifier,
|
||||
canInstall,
|
||||
onInstallSuccess,
|
||||
shouldDim,
|
||||
}
|
||||
}
|
||||
|
||||
const useDataSourceInstallation = (data: DataSourceNodeType): InstallationState => {
|
||||
const dataSourceList = useStore(s => s.dataSourceList)
|
||||
const invalidateDataSourceList = useInvalidDataSourceList()
|
||||
|
||||
const matchedPlugin = useMemo(() => {
|
||||
if (!dataSourceList || !dataSourceList.length)
|
||||
return undefined
|
||||
|
||||
return dataSourceList.find((item) => {
|
||||
if (data.plugin_unique_identifier && item.plugin_unique_identifier === data.plugin_unique_identifier)
|
||||
return true
|
||||
if (data.plugin_id && item.plugin_id === data.plugin_id)
|
||||
return true
|
||||
if (data.provider_name && item.provider === data.provider_name)
|
||||
return true
|
||||
return false
|
||||
})
|
||||
}, [data.plugin_id, data.plugin_unique_identifier, data.provider_name, dataSourceList])
|
||||
|
||||
const uniqueIdentifier = data.plugin_unique_identifier || data.plugin_id
|
||||
const canInstall = Boolean(data.plugin_unique_identifier)
|
||||
|
||||
const onInstallSuccess = useCallback(() => {
|
||||
invalidateDataSourceList()
|
||||
}, [invalidateDataSourceList])
|
||||
|
||||
const hasLoadedList = dataSourceList !== undefined
|
||||
|
||||
const shouldDim = !hasLoadedList || (hasLoadedList && !matchedPlugin)
|
||||
|
||||
return {
|
||||
isChecking: !hasLoadedList,
|
||||
isMissing: hasLoadedList && !matchedPlugin,
|
||||
uniqueIdentifier,
|
||||
canInstall,
|
||||
onInstallSuccess,
|
||||
shouldDim,
|
||||
}
|
||||
}
|
||||
|
||||
export const useNodePluginInstallation = (data: CommonNodeType): InstallationState => {
|
||||
const toolInstallation = useToolInstallation(data as ToolNodeType)
|
||||
const triggerInstallation = useTriggerInstallation(data as PluginTriggerNodeType)
|
||||
const dataSourceInstallation = useDataSourceInstallation(data as DataSourceNodeType)
|
||||
|
||||
switch (data.type as BlockEnum) {
|
||||
case BlockEnum.Tool:
|
||||
return toolInstallation
|
||||
case BlockEnum.TriggerPlugin:
|
||||
return triggerInstallation
|
||||
case BlockEnum.DataSource:
|
||||
return dataSourceInstallation
|
||||
default:
|
||||
return {
|
||||
isChecking: false,
|
||||
isMissing: false,
|
||||
uniqueIdentifier: undefined,
|
||||
canInstall: false,
|
||||
onInstallSuccess: () => undefined,
|
||||
shouldDim: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import { useCallback } from 'react'
|
||||
import {
|
||||
useIsChatMode,
|
||||
useWorkflow,
|
||||
useWorkflowVariables,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
import { BlockEnum, type Node, type NodeOutPutVar, type ValueSelector, type Var } from '@/app/components/workflow/types'
|
||||
type Params = {
|
||||
onlyLeafNodeVar?: boolean
|
||||
hideEnv?: boolean
|
||||
hideChatVar?: boolean
|
||||
filterVar: (payload: Var, selector: ValueSelector) => boolean
|
||||
passedInAvailableNodes?: Node[]
|
||||
}
|
||||
|
||||
const getNodeInfo = (nodeId: string, nodes: Node[]) => {
|
||||
const allNodes = nodes
|
||||
const node = allNodes.find(n => n.id === nodeId)
|
||||
const isInIteration = !!node?.data.isInIteration
|
||||
const isInLoop = !!node?.data.isInLoop
|
||||
const parentNodeId = node?.parentId
|
||||
const parentNode = allNodes.find(n => n.id === parentNodeId)
|
||||
return {
|
||||
node,
|
||||
isInIteration,
|
||||
isInLoop,
|
||||
parentNode,
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: loop type?
|
||||
const useNodesAvailableVarList = (nodes: Node[], {
|
||||
onlyLeafNodeVar,
|
||||
filterVar,
|
||||
hideEnv = false,
|
||||
hideChatVar = false,
|
||||
passedInAvailableNodes,
|
||||
}: Params = {
|
||||
onlyLeafNodeVar: false,
|
||||
filterVar: () => true,
|
||||
}) => {
|
||||
const { getTreeLeafNodes, getBeforeNodesInSameBranchIncludeParent } = useWorkflow()
|
||||
const { getNodeAvailableVars } = useWorkflowVariables()
|
||||
const isChatMode = useIsChatMode()
|
||||
|
||||
const nodeAvailabilityMap: { [key: string ]: { availableVars: NodeOutPutVar[], availableNodes: Node[] } } = {}
|
||||
|
||||
nodes.forEach((node) => {
|
||||
const nodeId = node.id
|
||||
const availableNodes = passedInAvailableNodes || (onlyLeafNodeVar ? getTreeLeafNodes(nodeId) : getBeforeNodesInSameBranchIncludeParent(nodeId))
|
||||
if (node.data.type === BlockEnum.Loop)
|
||||
availableNodes.push(node)
|
||||
|
||||
const {
|
||||
parentNode: iterationNode,
|
||||
} = getNodeInfo(nodeId, nodes)
|
||||
|
||||
const availableVars = getNodeAvailableVars({
|
||||
parentNode: iterationNode,
|
||||
beforeNodes: availableNodes,
|
||||
isChatMode,
|
||||
filterVar,
|
||||
hideEnv,
|
||||
hideChatVar,
|
||||
})
|
||||
const result = {
|
||||
node,
|
||||
availableVars,
|
||||
availableNodes,
|
||||
}
|
||||
nodeAvailabilityMap[nodeId] = result
|
||||
})
|
||||
return nodeAvailabilityMap
|
||||
}
|
||||
|
||||
export const useGetNodesAvailableVarList = () => {
|
||||
const { getTreeLeafNodes, getBeforeNodesInSameBranchIncludeParent } = useWorkflow()
|
||||
const { getNodeAvailableVars } = useWorkflowVariables()
|
||||
const isChatMode = useIsChatMode()
|
||||
const getNodesAvailableVarList = useCallback((nodes: Node[], {
|
||||
onlyLeafNodeVar,
|
||||
filterVar,
|
||||
hideEnv,
|
||||
hideChatVar,
|
||||
passedInAvailableNodes,
|
||||
}: Params = {
|
||||
onlyLeafNodeVar: false,
|
||||
filterVar: () => true,
|
||||
}) => {
|
||||
const nodeAvailabilityMap: { [key: string ]: { availableVars: NodeOutPutVar[], availableNodes: Node[] } } = {}
|
||||
|
||||
nodes.forEach((node) => {
|
||||
const nodeId = node.id
|
||||
const availableNodes = passedInAvailableNodes || (onlyLeafNodeVar ? getTreeLeafNodes(nodeId) : getBeforeNodesInSameBranchIncludeParent(nodeId))
|
||||
if (node.data.type === BlockEnum.Loop)
|
||||
availableNodes.push(node)
|
||||
|
||||
const {
|
||||
parentNode: iterationNode,
|
||||
} = getNodeInfo(nodeId, nodes)
|
||||
|
||||
const availableVars = getNodeAvailableVars({
|
||||
parentNode: iterationNode,
|
||||
beforeNodes: availableNodes,
|
||||
isChatMode,
|
||||
filterVar,
|
||||
hideEnv,
|
||||
hideChatVar,
|
||||
})
|
||||
const result = {
|
||||
node,
|
||||
availableVars,
|
||||
availableNodes,
|
||||
}
|
||||
nodeAvailabilityMap[nodeId] = result
|
||||
})
|
||||
return nodeAvailabilityMap
|
||||
}, [getTreeLeafNodes, getBeforeNodesInSameBranchIncludeParent, getNodeAvailableVars, isChatMode])
|
||||
return {
|
||||
getNodesAvailableVarList,
|
||||
}
|
||||
}
|
||||
|
||||
export default useNodesAvailableVarList
|
||||
@@ -0,0 +1,62 @@
|
||||
import { useCallback } from 'react'
|
||||
import { produce } from 'immer'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import { NodeRunningStatus } from '../types'
|
||||
|
||||
export const useNodesInteractionsWithoutSync = () => {
|
||||
const store = useStoreApi()
|
||||
|
||||
const handleNodeCancelRunningStatus = useCallback(() => {
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
} = store.getState()
|
||||
|
||||
const nodes = getNodes()
|
||||
const newNodes = produce(nodes, (draft) => {
|
||||
draft.forEach((node) => {
|
||||
node.data._runningStatus = undefined
|
||||
node.data._waitingRun = false
|
||||
})
|
||||
})
|
||||
setNodes(newNodes)
|
||||
}, [store])
|
||||
|
||||
const handleCancelAllNodeSuccessStatus = useCallback(() => {
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
} = store.getState()
|
||||
|
||||
const nodes = getNodes()
|
||||
const newNodes = produce(nodes, (draft) => {
|
||||
draft.forEach((node) => {
|
||||
if(node.data._runningStatus === NodeRunningStatus.Succeeded)
|
||||
node.data._runningStatus = undefined
|
||||
})
|
||||
})
|
||||
setNodes(newNodes)
|
||||
}, [store])
|
||||
|
||||
const handleCancelNodeSuccessStatus = useCallback((nodeId: string) => {
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
} = store.getState()
|
||||
|
||||
const newNodes = produce(getNodes(), (draft) => {
|
||||
const node = draft.find(n => n.id === nodeId)
|
||||
if (node && node.data._runningStatus === NodeRunningStatus.Succeeded) {
|
||||
node.data._runningStatus = undefined
|
||||
node.data._waitingRun = false
|
||||
}
|
||||
})
|
||||
setNodes(newNodes)
|
||||
}, [store])
|
||||
|
||||
return {
|
||||
handleNodeCancelRunningStatus,
|
||||
handleCancelAllNodeSuccessStatus,
|
||||
handleCancelNodeSuccessStatus,
|
||||
}
|
||||
}
|
||||
2016
dify/web/app/components/workflow/hooks/use-nodes-interactions.ts
Normal file
2016
dify/web/app/components/workflow/hooks/use-nodes-interactions.ts
Normal file
File diff suppressed because it is too large
Load Diff
96
dify/web/app/components/workflow/hooks/use-nodes-layout.ts
Normal file
96
dify/web/app/components/workflow/hooks/use-nodes-layout.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { useCallback } from 'react'
|
||||
import ELK from 'elkjs/lib/elk.bundled.js'
|
||||
import {
|
||||
useReactFlow,
|
||||
useStoreApi,
|
||||
} from 'reactflow'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import type {
|
||||
Edge,
|
||||
Node,
|
||||
} from '../types'
|
||||
import { useWorkflowStore } from '../store'
|
||||
import { AUTO_LAYOUT_OFFSET } from '../constants'
|
||||
import { useNodesSyncDraft } from './use-nodes-sync-draft'
|
||||
|
||||
const layoutOptions = {
|
||||
'elk.algorithm': 'layered',
|
||||
'elk.direction': 'RIGHT',
|
||||
'elk.layered.spacing.nodeNodeBetweenLayers': '60',
|
||||
'elk.spacing.nodeNode': '40',
|
||||
'elk.layered.nodePlacement.strategy': 'SIMPLE',
|
||||
}
|
||||
|
||||
const elk = new ELK()
|
||||
|
||||
export const getLayoutedNodes = async (nodes: Node[], edges: Edge[]) => {
|
||||
const graph = {
|
||||
id: 'root',
|
||||
layoutOptions,
|
||||
children: nodes.map((n) => {
|
||||
return {
|
||||
...n,
|
||||
width: n.width ?? 150,
|
||||
height: n.height ?? 50,
|
||||
targetPosition: 'left',
|
||||
sourcePosition: 'right',
|
||||
}
|
||||
}),
|
||||
edges: cloneDeep(edges),
|
||||
}
|
||||
|
||||
const layoutedGraph = await elk.layout(graph as any)
|
||||
const layoutedNodes = nodes.map((node) => {
|
||||
const layoutedNode = layoutedGraph.children?.find(
|
||||
lgNode => lgNode.id === node.id,
|
||||
)
|
||||
|
||||
return {
|
||||
...node,
|
||||
position: {
|
||||
x: (layoutedNode?.x ?? 0) + AUTO_LAYOUT_OFFSET.x,
|
||||
y: (layoutedNode?.y ?? 0) + AUTO_LAYOUT_OFFSET.y,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
layoutedNodes,
|
||||
}
|
||||
}
|
||||
|
||||
export const useNodesLayout = () => {
|
||||
const store = useStoreApi()
|
||||
const reactflow = useReactFlow()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
|
||||
const handleNodesLayout = useCallback(async () => {
|
||||
workflowStore.setState({ nodeAnimation: true })
|
||||
const {
|
||||
getNodes,
|
||||
edges,
|
||||
setNodes,
|
||||
} = store.getState()
|
||||
const { setViewport } = reactflow
|
||||
const nodes = getNodes()
|
||||
const {
|
||||
layoutedNodes,
|
||||
} = await getLayoutedNodes(nodes, edges)
|
||||
|
||||
setNodes(layoutedNodes)
|
||||
const zoom = 0.7
|
||||
setViewport({
|
||||
x: 0,
|
||||
y: 0,
|
||||
zoom,
|
||||
})
|
||||
setTimeout(() => {
|
||||
handleSyncWorkflowDraft()
|
||||
})
|
||||
}, [store, reactflow, handleSyncWorkflowDraft, workflowStore])
|
||||
|
||||
return {
|
||||
handleNodesLayout,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { useMemo } from 'react'
|
||||
import type { AvailableNodesMetaData } from '@/app/components/workflow/hooks-store'
|
||||
import { useHooksStore } from '@/app/components/workflow/hooks-store'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import type { Node } from '@/app/components/workflow/types'
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import { canFindTool } from '@/utils'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import {
|
||||
useAllBuiltInTools,
|
||||
useAllCustomTools,
|
||||
useAllWorkflowTools,
|
||||
} from '@/service/use-tools'
|
||||
|
||||
export const useNodesMetaData = () => {
|
||||
const availableNodesMetaData = useHooksStore(s => s.availableNodesMetaData)
|
||||
|
||||
return useMemo(() => {
|
||||
return {
|
||||
nodes: availableNodesMetaData?.nodes || [],
|
||||
nodesMap: availableNodesMetaData?.nodesMap || {},
|
||||
} as AvailableNodesMetaData
|
||||
}, [availableNodesMetaData])
|
||||
}
|
||||
|
||||
export const useNodeMetaData = (node: Node) => {
|
||||
const language = useGetLanguage()
|
||||
const { data: buildInTools } = useAllBuiltInTools()
|
||||
const { data: customTools } = useAllCustomTools()
|
||||
const { data: workflowTools } = useAllWorkflowTools()
|
||||
const dataSourceList = useStore(s => s.dataSourceList)
|
||||
const availableNodesMetaData = useNodesMetaData()
|
||||
const { data } = node
|
||||
const nodeMetaData = availableNodesMetaData.nodesMap?.[data.type]
|
||||
const author = useMemo(() => {
|
||||
if (data.type === BlockEnum.DataSource)
|
||||
return dataSourceList?.find(dataSource => dataSource.plugin_id === data.plugin_id)?.author
|
||||
|
||||
if (data.type === BlockEnum.Tool) {
|
||||
if (data.provider_type === CollectionType.builtIn)
|
||||
return buildInTools?.find(toolWithProvider => canFindTool(toolWithProvider.id, data.provider_id))?.author
|
||||
if (data.provider_type === CollectionType.workflow)
|
||||
return workflowTools?.find(toolWithProvider => toolWithProvider.id === data.provider_id)?.author
|
||||
return customTools?.find(toolWithProvider => toolWithProvider.id === data.provider_id)?.author
|
||||
}
|
||||
return nodeMetaData?.metaData.author
|
||||
}, [data, buildInTools, customTools, workflowTools, nodeMetaData, dataSourceList])
|
||||
|
||||
const description = useMemo(() => {
|
||||
if (data.type === BlockEnum.DataSource)
|
||||
return dataSourceList?.find(dataSource => dataSource.plugin_id === data.plugin_id)?.description[language]
|
||||
if (data.type === BlockEnum.Tool) {
|
||||
if (data.provider_type === CollectionType.builtIn)
|
||||
return buildInTools?.find(toolWithProvider => canFindTool(toolWithProvider.id, data.provider_id))?.description[language]
|
||||
if (data.provider_type === CollectionType.workflow)
|
||||
return workflowTools?.find(toolWithProvider => toolWithProvider.id === data.provider_id)?.description[language]
|
||||
return customTools?.find(toolWithProvider => toolWithProvider.id === data.provider_id)?.description[language]
|
||||
}
|
||||
return nodeMetaData?.metaData.description
|
||||
}, [data, buildInTools, customTools, workflowTools, nodeMetaData, dataSourceList, language])
|
||||
|
||||
return useMemo(() => {
|
||||
return {
|
||||
...nodeMetaData?.metaData,
|
||||
author,
|
||||
description,
|
||||
}
|
||||
}, [author, nodeMetaData, description])
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { useCallback } from 'react'
|
||||
import { useStore } from '../store'
|
||||
import { useNodesReadOnly } from './use-workflow'
|
||||
import { useHooksStore } from '@/app/components/workflow/hooks-store'
|
||||
|
||||
export type SyncCallback = {
|
||||
onSuccess?: () => void
|
||||
onError?: () => void
|
||||
onSettled?: () => void
|
||||
}
|
||||
|
||||
export const useNodesSyncDraft = () => {
|
||||
const { getNodesReadOnly } = useNodesReadOnly()
|
||||
const debouncedSyncWorkflowDraft = useStore(s => s.debouncedSyncWorkflowDraft)
|
||||
const doSyncWorkflowDraft = useHooksStore(s => s.doSyncWorkflowDraft)
|
||||
const syncWorkflowDraftWhenPageClose = useHooksStore(s => s.syncWorkflowDraftWhenPageClose)
|
||||
|
||||
const handleSyncWorkflowDraft = useCallback((
|
||||
sync?: boolean,
|
||||
notRefreshWhenSyncError?: boolean,
|
||||
callback?: SyncCallback,
|
||||
) => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
if (sync)
|
||||
doSyncWorkflowDraft(notRefreshWhenSyncError, callback)
|
||||
else
|
||||
debouncedSyncWorkflowDraft(doSyncWorkflowDraft)
|
||||
}, [debouncedSyncWorkflowDraft, doSyncWorkflowDraft, getNodesReadOnly])
|
||||
|
||||
return {
|
||||
doSyncWorkflowDraft,
|
||||
handleSyncWorkflowDraft,
|
||||
syncWorkflowDraftWhenPageClose,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { MouseEvent } from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import { useWorkflowStore } from '../store'
|
||||
|
||||
export const usePanelInteractions = () => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
const handlePaneContextMenu = useCallback((e: MouseEvent) => {
|
||||
e.preventDefault()
|
||||
const container = document.querySelector('#workflow-container')
|
||||
const { x, y } = container!.getBoundingClientRect()
|
||||
workflowStore.setState({
|
||||
panelMenu: {
|
||||
top: e.clientY - y,
|
||||
left: e.clientX - x,
|
||||
},
|
||||
})
|
||||
}, [workflowStore])
|
||||
|
||||
const handlePaneContextmenuCancel = useCallback(() => {
|
||||
workflowStore.setState({
|
||||
panelMenu: undefined,
|
||||
})
|
||||
}, [workflowStore])
|
||||
|
||||
const handleNodeContextmenuCancel = useCallback(() => {
|
||||
workflowStore.setState({
|
||||
nodeMenu: undefined,
|
||||
})
|
||||
}, [workflowStore])
|
||||
|
||||
return {
|
||||
handlePaneContextMenu,
|
||||
handlePaneContextmenuCancel,
|
||||
handleNodeContextmenuCancel,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
import type { MouseEvent } from 'react'
|
||||
import {
|
||||
useCallback,
|
||||
} from 'react'
|
||||
import { produce } from 'immer'
|
||||
import type {
|
||||
OnSelectionChangeFunc,
|
||||
} from 'reactflow'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import { useWorkflowStore } from '../store'
|
||||
import type { Node } from '../types'
|
||||
|
||||
export const useSelectionInteractions = () => {
|
||||
const store = useStoreApi()
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
const handleSelectionStart = useCallback(() => {
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
edges,
|
||||
setEdges,
|
||||
userSelectionRect,
|
||||
} = store.getState()
|
||||
|
||||
if (!userSelectionRect?.width || !userSelectionRect?.height) {
|
||||
const nodes = getNodes()
|
||||
const newNodes = produce(nodes, (draft) => {
|
||||
draft.forEach((node) => {
|
||||
if (node.data._isBundled)
|
||||
node.data._isBundled = false
|
||||
})
|
||||
})
|
||||
setNodes(newNodes)
|
||||
const newEdges = produce(edges, (draft) => {
|
||||
draft.forEach((edge) => {
|
||||
if (edge.data._isBundled)
|
||||
edge.data._isBundled = false
|
||||
})
|
||||
})
|
||||
setEdges(newEdges)
|
||||
}
|
||||
}, [store])
|
||||
|
||||
const handleSelectionChange = useCallback<OnSelectionChangeFunc>(({ nodes: nodesInSelection, edges: edgesInSelection }) => {
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
edges,
|
||||
setEdges,
|
||||
userSelectionRect,
|
||||
} = store.getState()
|
||||
|
||||
const nodes = getNodes()
|
||||
|
||||
if (!userSelectionRect?.width || !userSelectionRect?.height)
|
||||
return
|
||||
|
||||
const newNodes = produce(nodes, (draft) => {
|
||||
draft.forEach((node) => {
|
||||
const nodeInSelection = nodesInSelection.find(n => n.id === node.id)
|
||||
|
||||
if (nodeInSelection)
|
||||
node.data._isBundled = true
|
||||
else
|
||||
node.data._isBundled = false
|
||||
})
|
||||
})
|
||||
setNodes(newNodes)
|
||||
const newEdges = produce(edges, (draft) => {
|
||||
draft.forEach((edge) => {
|
||||
const edgeInSelection = edgesInSelection.find(e => e.id === edge.id)
|
||||
|
||||
if (edgeInSelection)
|
||||
edge.data._isBundled = true
|
||||
else
|
||||
edge.data._isBundled = false
|
||||
})
|
||||
})
|
||||
setEdges(newEdges)
|
||||
}, [store])
|
||||
|
||||
const handleSelectionDrag = useCallback((_: MouseEvent, nodesWithDrag: Node[]) => {
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
} = store.getState()
|
||||
|
||||
workflowStore.setState({
|
||||
nodeAnimation: false,
|
||||
})
|
||||
const nodes = getNodes()
|
||||
const newNodes = produce(nodes, (draft) => {
|
||||
draft.forEach((node) => {
|
||||
const dragNode = nodesWithDrag.find(n => n.id === node.id)
|
||||
|
||||
if (dragNode)
|
||||
node.position = dragNode.position
|
||||
})
|
||||
})
|
||||
setNodes(newNodes)
|
||||
}, [store, workflowStore])
|
||||
|
||||
const handleSelectionCancel = useCallback(() => {
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
edges,
|
||||
setEdges,
|
||||
} = store.getState()
|
||||
|
||||
store.setState({
|
||||
userSelectionRect: null,
|
||||
userSelectionActive: true,
|
||||
})
|
||||
|
||||
const nodes = getNodes()
|
||||
const newNodes = produce(nodes, (draft) => {
|
||||
draft.forEach((node) => {
|
||||
if (node.data._isBundled)
|
||||
node.data._isBundled = false
|
||||
})
|
||||
})
|
||||
setNodes(newNodes)
|
||||
const newEdges = produce(edges, (draft) => {
|
||||
draft.forEach((edge) => {
|
||||
if (edge.data._isBundled)
|
||||
edge.data._isBundled = false
|
||||
})
|
||||
})
|
||||
setEdges(newEdges)
|
||||
}, [store])
|
||||
|
||||
const handleSelectionContextMenu = useCallback((e: MouseEvent) => {
|
||||
const target = e.target as HTMLElement
|
||||
if (!target.classList.contains('react-flow__nodesselection-rect'))
|
||||
return
|
||||
|
||||
e.preventDefault()
|
||||
const container = document.querySelector('#workflow-container')
|
||||
const { x, y } = container!.getBoundingClientRect()
|
||||
workflowStore.setState({
|
||||
selectionMenu: {
|
||||
top: e.clientY - y,
|
||||
left: e.clientX - x,
|
||||
},
|
||||
})
|
||||
}, [workflowStore])
|
||||
|
||||
const handleSelectionContextmenuCancel = useCallback(() => {
|
||||
workflowStore.setState({
|
||||
selectionMenu: undefined,
|
||||
})
|
||||
}, [workflowStore])
|
||||
|
||||
return {
|
||||
handleSelectionStart,
|
||||
handleSelectionChange,
|
||||
handleSelectionDrag,
|
||||
handleSelectionCancel,
|
||||
handleSelectionContextMenu,
|
||||
handleSelectionContextmenuCancel,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import {
|
||||
useCallback,
|
||||
useRef,
|
||||
} from 'react'
|
||||
|
||||
export const useSerialAsyncCallback = <Args extends any[], Result = void>(
|
||||
fn: (...args: Args) => Promise<Result> | Result,
|
||||
shouldSkip?: () => boolean,
|
||||
) => {
|
||||
const queueRef = useRef<Promise<unknown>>(Promise.resolve())
|
||||
|
||||
return useCallback((...args: Args) => {
|
||||
if (shouldSkip?.())
|
||||
return Promise.resolve(undefined as Result)
|
||||
|
||||
const lastPromise = queueRef.current.catch(() => undefined)
|
||||
const nextPromise = lastPromise.then(() => fn(...args))
|
||||
queueRef.current = nextPromise
|
||||
|
||||
return nextPromise
|
||||
}, [fn, shouldSkip])
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { useHooksStore } from '@/app/components/workflow/hooks-store'
|
||||
|
||||
export const useSetWorkflowVarsWithValue = () => {
|
||||
const fetchInspectVars = useHooksStore(s => s.fetchInspectVars)
|
||||
|
||||
return {
|
||||
fetchInspectVars,
|
||||
}
|
||||
}
|
||||
249
dify/web/app/components/workflow/hooks/use-shortcuts.ts
Normal file
249
dify/web/app/components/workflow/hooks/use-shortcuts.ts
Normal file
@@ -0,0 +1,249 @@
|
||||
import { useReactFlow } from 'reactflow'
|
||||
import { useKeyPress } from 'ahooks'
|
||||
import { useCallback } from 'react'
|
||||
import {
|
||||
getKeyboardKeyCodeBySystem,
|
||||
isEventTargetInputArea,
|
||||
} from '../utils'
|
||||
import { useWorkflowHistoryStore } from '../workflow-history-store'
|
||||
import { useWorkflowStore } from '../store'
|
||||
import {
|
||||
useEdgesInteractions,
|
||||
useNodesInteractions,
|
||||
useNodesSyncDraft,
|
||||
useWorkflowCanvasMaximize,
|
||||
useWorkflowMoveMode,
|
||||
useWorkflowOrganize,
|
||||
} from '.'
|
||||
|
||||
export const useShortcuts = (): void => {
|
||||
const {
|
||||
handleNodesCopy,
|
||||
handleNodesPaste,
|
||||
handleNodesDuplicate,
|
||||
handleNodesDelete,
|
||||
handleHistoryBack,
|
||||
handleHistoryForward,
|
||||
dimOtherNodes,
|
||||
undimAllNodes,
|
||||
} = useNodesInteractions()
|
||||
const { shortcutsEnabled: workflowHistoryShortcutsEnabled } = useWorkflowHistoryStore()
|
||||
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
const { handleEdgeDelete } = useEdgesInteractions()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const {
|
||||
handleModeHand,
|
||||
handleModePointer,
|
||||
} = useWorkflowMoveMode()
|
||||
const { handleLayout } = useWorkflowOrganize()
|
||||
const { handleToggleMaximizeCanvas } = useWorkflowCanvasMaximize()
|
||||
|
||||
const {
|
||||
zoomTo,
|
||||
getZoom,
|
||||
fitView,
|
||||
} = useReactFlow()
|
||||
|
||||
// Zoom out to a minimum of 0.25 for shortcut
|
||||
const constrainedZoomOut = () => {
|
||||
const currentZoom = getZoom()
|
||||
const newZoom = Math.max(currentZoom - 0.1, 0.25)
|
||||
zoomTo(newZoom)
|
||||
}
|
||||
|
||||
// Zoom in to a maximum of 2 for shortcut
|
||||
const constrainedZoomIn = () => {
|
||||
const currentZoom = getZoom()
|
||||
const newZoom = Math.min(currentZoom + 0.1, 2)
|
||||
zoomTo(newZoom)
|
||||
}
|
||||
|
||||
const shouldHandleShortcut = useCallback((e: KeyboardEvent) => {
|
||||
return !isEventTargetInputArea(e.target as HTMLElement)
|
||||
}, [])
|
||||
|
||||
useKeyPress(['delete', 'backspace'], (e) => {
|
||||
if (shouldHandleShortcut(e)) {
|
||||
e.preventDefault()
|
||||
handleNodesDelete()
|
||||
handleEdgeDelete()
|
||||
}
|
||||
})
|
||||
|
||||
useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.c`, (e) => {
|
||||
const { showDebugAndPreviewPanel } = workflowStore.getState()
|
||||
if (shouldHandleShortcut(e) && !showDebugAndPreviewPanel) {
|
||||
e.preventDefault()
|
||||
handleNodesCopy()
|
||||
}
|
||||
}, { exactMatch: true, useCapture: true })
|
||||
|
||||
useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.v`, (e) => {
|
||||
const { showDebugAndPreviewPanel } = workflowStore.getState()
|
||||
if (shouldHandleShortcut(e) && !showDebugAndPreviewPanel) {
|
||||
e.preventDefault()
|
||||
handleNodesPaste()
|
||||
}
|
||||
}, { exactMatch: true, useCapture: true })
|
||||
|
||||
useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.d`, (e) => {
|
||||
if (shouldHandleShortcut(e)) {
|
||||
e.preventDefault()
|
||||
handleNodesDuplicate()
|
||||
}
|
||||
}, { exactMatch: true, useCapture: true })
|
||||
|
||||
useKeyPress(`${getKeyboardKeyCodeBySystem('alt')}.r`, (e) => {
|
||||
if (shouldHandleShortcut(e)) {
|
||||
e.preventDefault()
|
||||
// @ts-expect-error - Dynamic property added by run-and-history component
|
||||
if (window._toggleTestRunDropdown) {
|
||||
// @ts-expect-error - Dynamic property added by run-and-history component
|
||||
window._toggleTestRunDropdown()
|
||||
}
|
||||
}
|
||||
}, { exactMatch: true, useCapture: true })
|
||||
|
||||
useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.z`, (e) => {
|
||||
const { showDebugAndPreviewPanel } = workflowStore.getState()
|
||||
if (shouldHandleShortcut(e) && !showDebugAndPreviewPanel) {
|
||||
e.preventDefault()
|
||||
if (workflowHistoryShortcutsEnabled)
|
||||
handleHistoryBack()
|
||||
}
|
||||
}, { exactMatch: true, useCapture: true })
|
||||
|
||||
useKeyPress(
|
||||
[`${getKeyboardKeyCodeBySystem('ctrl')}.y`, `${getKeyboardKeyCodeBySystem('ctrl')}.shift.z`],
|
||||
(e) => {
|
||||
if (shouldHandleShortcut(e)) {
|
||||
e.preventDefault()
|
||||
if (workflowHistoryShortcutsEnabled)
|
||||
handleHistoryForward()
|
||||
}
|
||||
},
|
||||
{ exactMatch: true, useCapture: true },
|
||||
)
|
||||
|
||||
useKeyPress('h', (e) => {
|
||||
if (shouldHandleShortcut(e)) {
|
||||
e.preventDefault()
|
||||
handleModeHand()
|
||||
}
|
||||
}, {
|
||||
exactMatch: true,
|
||||
useCapture: true,
|
||||
})
|
||||
|
||||
useKeyPress('v', (e) => {
|
||||
if (shouldHandleShortcut(e)) {
|
||||
e.preventDefault()
|
||||
handleModePointer()
|
||||
}
|
||||
}, {
|
||||
exactMatch: true,
|
||||
useCapture: true,
|
||||
})
|
||||
|
||||
useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.o`, (e) => {
|
||||
if (shouldHandleShortcut(e)) {
|
||||
e.preventDefault()
|
||||
handleLayout()
|
||||
}
|
||||
}, { exactMatch: true, useCapture: true })
|
||||
|
||||
useKeyPress('f', (e) => {
|
||||
if (shouldHandleShortcut(e)) {
|
||||
e.preventDefault()
|
||||
handleToggleMaximizeCanvas()
|
||||
}
|
||||
}, {
|
||||
exactMatch: true,
|
||||
useCapture: true,
|
||||
})
|
||||
|
||||
useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.1`, (e) => {
|
||||
if (shouldHandleShortcut(e)) {
|
||||
e.preventDefault()
|
||||
fitView()
|
||||
handleSyncWorkflowDraft()
|
||||
}
|
||||
}, {
|
||||
exactMatch: true,
|
||||
useCapture: true,
|
||||
})
|
||||
|
||||
useKeyPress('shift.1', (e) => {
|
||||
if (shouldHandleShortcut(e)) {
|
||||
e.preventDefault()
|
||||
zoomTo(1)
|
||||
handleSyncWorkflowDraft()
|
||||
}
|
||||
}, {
|
||||
exactMatch: true,
|
||||
useCapture: true,
|
||||
})
|
||||
|
||||
useKeyPress('shift.5', (e) => {
|
||||
if (shouldHandleShortcut(e)) {
|
||||
e.preventDefault()
|
||||
zoomTo(0.5)
|
||||
handleSyncWorkflowDraft()
|
||||
}
|
||||
}, {
|
||||
exactMatch: true,
|
||||
useCapture: true,
|
||||
})
|
||||
|
||||
useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.dash`, (e) => {
|
||||
if (shouldHandleShortcut(e)) {
|
||||
e.preventDefault()
|
||||
constrainedZoomOut()
|
||||
handleSyncWorkflowDraft()
|
||||
}
|
||||
}, {
|
||||
exactMatch: true,
|
||||
useCapture: true,
|
||||
})
|
||||
|
||||
useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.equalsign`, (e) => {
|
||||
if (shouldHandleShortcut(e)) {
|
||||
e.preventDefault()
|
||||
constrainedZoomIn()
|
||||
handleSyncWorkflowDraft()
|
||||
}
|
||||
}, {
|
||||
exactMatch: true,
|
||||
useCapture: true,
|
||||
})
|
||||
|
||||
// Shift ↓
|
||||
useKeyPress(
|
||||
'shift',
|
||||
(e) => {
|
||||
if (shouldHandleShortcut(e))
|
||||
dimOtherNodes()
|
||||
},
|
||||
{
|
||||
exactMatch: true,
|
||||
useCapture: true,
|
||||
events: ['keydown'],
|
||||
},
|
||||
)
|
||||
|
||||
// Shift ↑
|
||||
useKeyPress(
|
||||
(e) => {
|
||||
return e.key === 'Shift'
|
||||
},
|
||||
(e) => {
|
||||
if (shouldHandleShortcut(e))
|
||||
undimAllNodes()
|
||||
},
|
||||
{
|
||||
exactMatch: true,
|
||||
useCapture: true,
|
||||
events: ['keyup'],
|
||||
},
|
||||
)
|
||||
}
|
||||
202
dify/web/app/components/workflow/hooks/use-tool-icon.ts
Normal file
202
dify/web/app/components/workflow/hooks/use-tool-icon.ts
Normal file
@@ -0,0 +1,202 @@
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import type { Node, ToolWithProvider } from '../types'
|
||||
import { BlockEnum } from '../types'
|
||||
import { useStore, useWorkflowStore } from '../store'
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
import { canFindTool } from '@/utils'
|
||||
import {
|
||||
useAllBuiltInTools,
|
||||
useAllCustomTools,
|
||||
useAllMCPTools,
|
||||
useAllWorkflowTools,
|
||||
} from '@/service/use-tools'
|
||||
import { useAllTriggerPlugins } from '@/service/use-triggers'
|
||||
import type { PluginTriggerNodeType } from '../nodes/trigger-plugin/types'
|
||||
import type { ToolNodeType } from '../nodes/tool/types'
|
||||
import type { DataSourceNodeType } from '../nodes/data-source/types'
|
||||
import type { TriggerWithProvider } from '../block-selector/types'
|
||||
|
||||
const isTriggerPluginNode = (data: Node['data']): data is PluginTriggerNodeType => data.type === BlockEnum.TriggerPlugin
|
||||
|
||||
const isToolNode = (data: Node['data']): data is ToolNodeType => data.type === BlockEnum.Tool
|
||||
|
||||
const isDataSourceNode = (data: Node['data']): data is DataSourceNodeType => data.type === BlockEnum.DataSource
|
||||
|
||||
const findTriggerPluginIcon = (
|
||||
identifiers: (string | undefined)[],
|
||||
triggers: TriggerWithProvider[] | undefined,
|
||||
) => {
|
||||
const targetTriggers = triggers || []
|
||||
for (const identifier of identifiers) {
|
||||
if (!identifier)
|
||||
continue
|
||||
const matched = targetTriggers.find(trigger => trigger.id === identifier || canFindTool(trigger.id, identifier))
|
||||
if (matched?.icon)
|
||||
return matched.icon
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
export const useToolIcon = (data?: Node['data']) => {
|
||||
const { data: buildInTools } = useAllBuiltInTools()
|
||||
const { data: customTools } = useAllCustomTools()
|
||||
const { data: workflowTools } = useAllWorkflowTools()
|
||||
const { data: mcpTools } = useAllMCPTools()
|
||||
const dataSourceList = useStore(s => s.dataSourceList)
|
||||
const { data: triggerPlugins } = useAllTriggerPlugins()
|
||||
|
||||
const toolIcon = useMemo(() => {
|
||||
if (!data)
|
||||
return ''
|
||||
|
||||
if (isTriggerPluginNode(data)) {
|
||||
const icon = findTriggerPluginIcon(
|
||||
[
|
||||
data.plugin_id,
|
||||
data.provider_id,
|
||||
data.provider_name,
|
||||
],
|
||||
triggerPlugins,
|
||||
)
|
||||
if (icon)
|
||||
return icon
|
||||
}
|
||||
|
||||
if (isToolNode(data)) {
|
||||
let primaryCollection: ToolWithProvider[] | undefined
|
||||
switch (data.provider_type) {
|
||||
case CollectionType.custom:
|
||||
primaryCollection = customTools
|
||||
break
|
||||
case CollectionType.mcp:
|
||||
primaryCollection = mcpTools
|
||||
break
|
||||
case CollectionType.workflow:
|
||||
primaryCollection = workflowTools
|
||||
break
|
||||
case CollectionType.builtIn:
|
||||
default:
|
||||
primaryCollection = buildInTools
|
||||
break
|
||||
}
|
||||
|
||||
const collectionsToSearch = [
|
||||
primaryCollection,
|
||||
buildInTools,
|
||||
customTools,
|
||||
workflowTools,
|
||||
mcpTools,
|
||||
] as Array<ToolWithProvider[] | undefined>
|
||||
|
||||
const seen = new Set<ToolWithProvider[]>()
|
||||
for (const collection of collectionsToSearch) {
|
||||
if (!collection || seen.has(collection))
|
||||
continue
|
||||
seen.add(collection)
|
||||
const matched = collection.find((toolWithProvider) => {
|
||||
if (canFindTool(toolWithProvider.id, data.provider_id))
|
||||
return true
|
||||
if (data.plugin_id && toolWithProvider.plugin_id === data.plugin_id)
|
||||
return true
|
||||
return data.provider_name === toolWithProvider.name
|
||||
})
|
||||
if (matched?.icon)
|
||||
return matched.icon
|
||||
}
|
||||
|
||||
if (data.provider_icon)
|
||||
return data.provider_icon
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
if (isDataSourceNode(data))
|
||||
return dataSourceList?.find(toolWithProvider => toolWithProvider.plugin_id === data.plugin_id)?.icon || ''
|
||||
|
||||
return ''
|
||||
}, [data, dataSourceList, buildInTools, customTools, workflowTools, mcpTools, triggerPlugins])
|
||||
|
||||
return toolIcon
|
||||
}
|
||||
|
||||
export const useGetToolIcon = () => {
|
||||
const { data: buildInTools } = useAllBuiltInTools()
|
||||
const { data: customTools } = useAllCustomTools()
|
||||
const { data: workflowTools } = useAllWorkflowTools()
|
||||
const { data: mcpTools } = useAllMCPTools()
|
||||
const { data: triggerPlugins } = useAllTriggerPlugins()
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
const getToolIcon = useCallback((data: Node['data']) => {
|
||||
const {
|
||||
buildInTools: storeBuiltInTools,
|
||||
customTools: storeCustomTools,
|
||||
workflowTools: storeWorkflowTools,
|
||||
mcpTools: storeMcpTools,
|
||||
dataSourceList,
|
||||
} = workflowStore.getState()
|
||||
|
||||
if (isTriggerPluginNode(data)) {
|
||||
return findTriggerPluginIcon(
|
||||
[
|
||||
data.plugin_id,
|
||||
data.provider_id,
|
||||
data.provider_name,
|
||||
],
|
||||
triggerPlugins,
|
||||
)
|
||||
}
|
||||
|
||||
if (isToolNode(data)) {
|
||||
const primaryCollection = (() => {
|
||||
switch (data.provider_type) {
|
||||
case CollectionType.custom:
|
||||
return storeCustomTools ?? customTools
|
||||
case CollectionType.mcp:
|
||||
return storeMcpTools ?? mcpTools
|
||||
case CollectionType.workflow:
|
||||
return storeWorkflowTools ?? workflowTools
|
||||
case CollectionType.builtIn:
|
||||
default:
|
||||
return storeBuiltInTools ?? buildInTools
|
||||
}
|
||||
})()
|
||||
|
||||
const collectionsToSearch = [
|
||||
primaryCollection,
|
||||
storeBuiltInTools ?? buildInTools,
|
||||
storeCustomTools ?? customTools,
|
||||
storeWorkflowTools ?? workflowTools,
|
||||
storeMcpTools ?? mcpTools,
|
||||
] as Array<ToolWithProvider[] | undefined>
|
||||
|
||||
const seen = new Set<ToolWithProvider[]>()
|
||||
for (const collection of collectionsToSearch) {
|
||||
if (!collection || seen.has(collection))
|
||||
continue
|
||||
seen.add(collection)
|
||||
const matched = collection.find((toolWithProvider) => {
|
||||
if (canFindTool(toolWithProvider.id, data.provider_id))
|
||||
return true
|
||||
if (data.plugin_id && toolWithProvider.plugin_id === data.plugin_id)
|
||||
return true
|
||||
return data.provider_name === toolWithProvider.name
|
||||
})
|
||||
if (matched?.icon)
|
||||
return matched.icon
|
||||
}
|
||||
|
||||
if (data.provider_icon)
|
||||
return data.provider_icon
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (isDataSourceNode(data))
|
||||
return dataSourceList?.find(toolWithProvider => toolWithProvider.plugin_id === data.plugin_id)?.icon
|
||||
|
||||
return undefined
|
||||
}, [workflowStore, triggerPlugins, buildInTools, customTools, workflowTools, mcpTools])
|
||||
|
||||
return getToolIcon
|
||||
}
|
||||
154
dify/web/app/components/workflow/hooks/use-workflow-history.ts
Normal file
154
dify/web/app/components/workflow/hooks/use-workflow-history.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import {
|
||||
useCallback,
|
||||
useRef, useState,
|
||||
} from 'react'
|
||||
import { debounce } from 'lodash-es'
|
||||
import {
|
||||
useStoreApi,
|
||||
} from 'reactflow'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useWorkflowHistoryStore } from '../workflow-history-store'
|
||||
import type { WorkflowHistoryEventMeta } from '../workflow-history-store'
|
||||
|
||||
/**
|
||||
* All supported Events that create a new history state.
|
||||
* Current limitations:
|
||||
* - InputChange events in Node Panels do not trigger state changes.
|
||||
* - Resizing UI elements does not trigger state changes.
|
||||
*/
|
||||
export const WorkflowHistoryEvent = {
|
||||
NodeTitleChange: 'NodeTitleChange',
|
||||
NodeDescriptionChange: 'NodeDescriptionChange',
|
||||
NodeDragStop: 'NodeDragStop',
|
||||
NodeChange: 'NodeChange',
|
||||
NodeConnect: 'NodeConnect',
|
||||
NodePaste: 'NodePaste',
|
||||
NodeDelete: 'NodeDelete',
|
||||
EdgeDelete: 'EdgeDelete',
|
||||
EdgeDeleteByDeleteBranch: 'EdgeDeleteByDeleteBranch',
|
||||
NodeAdd: 'NodeAdd',
|
||||
NodeResize: 'NodeResize',
|
||||
NoteAdd: 'NoteAdd',
|
||||
NoteChange: 'NoteChange',
|
||||
NoteDelete: 'NoteDelete',
|
||||
LayoutOrganize: 'LayoutOrganize',
|
||||
} as const
|
||||
|
||||
export type WorkflowHistoryEventT = keyof typeof WorkflowHistoryEvent
|
||||
|
||||
export const useWorkflowHistory = () => {
|
||||
const store = useStoreApi()
|
||||
const { store: workflowHistoryStore } = useWorkflowHistoryStore()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [undoCallbacks, setUndoCallbacks] = useState<(() => void)[]>([])
|
||||
const [redoCallbacks, setRedoCallbacks] = useState<(() => void)[]>([])
|
||||
|
||||
const onUndo = useCallback((callback: () => void) => {
|
||||
setUndoCallbacks(prev => [...prev, callback])
|
||||
return () => setUndoCallbacks(prev => prev.filter(cb => cb !== callback))
|
||||
}, [])
|
||||
|
||||
const onRedo = useCallback((callback: () => void) => {
|
||||
setRedoCallbacks(prev => [...prev, callback])
|
||||
return () => setRedoCallbacks(prev => prev.filter(cb => cb !== callback))
|
||||
}, [])
|
||||
|
||||
const undo = useCallback(() => {
|
||||
workflowHistoryStore.temporal.getState().undo()
|
||||
undoCallbacks.forEach(callback => callback())
|
||||
}, [undoCallbacks, workflowHistoryStore.temporal])
|
||||
|
||||
const redo = useCallback(() => {
|
||||
workflowHistoryStore.temporal.getState().redo()
|
||||
redoCallbacks.forEach(callback => callback())
|
||||
}, [redoCallbacks, workflowHistoryStore.temporal])
|
||||
|
||||
// Some events may be triggered multiple times in a short period of time.
|
||||
// We debounce the history state update to avoid creating multiple history states
|
||||
// with minimal changes.
|
||||
const saveStateToHistoryRef = useRef(debounce((event: WorkflowHistoryEventT, meta?: WorkflowHistoryEventMeta) => {
|
||||
workflowHistoryStore.setState({
|
||||
workflowHistoryEvent: event,
|
||||
workflowHistoryEventMeta: meta,
|
||||
nodes: store.getState().getNodes(),
|
||||
edges: store.getState().edges,
|
||||
})
|
||||
}, 500))
|
||||
|
||||
const saveStateToHistory = useCallback((event: WorkflowHistoryEventT, meta?: WorkflowHistoryEventMeta) => {
|
||||
switch (event) {
|
||||
case WorkflowHistoryEvent.NoteChange:
|
||||
// Hint: Note change does not trigger when note text changes,
|
||||
// because the note editors have their own history states.
|
||||
saveStateToHistoryRef.current(event, meta)
|
||||
break
|
||||
case WorkflowHistoryEvent.NodeTitleChange:
|
||||
case WorkflowHistoryEvent.NodeDescriptionChange:
|
||||
case WorkflowHistoryEvent.NodeDragStop:
|
||||
case WorkflowHistoryEvent.NodeChange:
|
||||
case WorkflowHistoryEvent.NodeConnect:
|
||||
case WorkflowHistoryEvent.NodePaste:
|
||||
case WorkflowHistoryEvent.NodeDelete:
|
||||
case WorkflowHistoryEvent.EdgeDelete:
|
||||
case WorkflowHistoryEvent.EdgeDeleteByDeleteBranch:
|
||||
case WorkflowHistoryEvent.NodeAdd:
|
||||
case WorkflowHistoryEvent.NodeResize:
|
||||
case WorkflowHistoryEvent.NoteAdd:
|
||||
case WorkflowHistoryEvent.LayoutOrganize:
|
||||
case WorkflowHistoryEvent.NoteDelete:
|
||||
saveStateToHistoryRef.current(event, meta)
|
||||
break
|
||||
default:
|
||||
// We do not create a history state for every event.
|
||||
// Some events of reactflow may change things the user would not want to undo/redo.
|
||||
// For example: UI state changes like selecting a node.
|
||||
break
|
||||
}
|
||||
}, [])
|
||||
|
||||
const getHistoryLabel = useCallback((event: WorkflowHistoryEventT) => {
|
||||
switch (event) {
|
||||
case WorkflowHistoryEvent.NodeTitleChange:
|
||||
return t('workflow.changeHistory.nodeTitleChange')
|
||||
case WorkflowHistoryEvent.NodeDescriptionChange:
|
||||
return t('workflow.changeHistory.nodeDescriptionChange')
|
||||
case WorkflowHistoryEvent.LayoutOrganize:
|
||||
case WorkflowHistoryEvent.NodeDragStop:
|
||||
return t('workflow.changeHistory.nodeDragStop')
|
||||
case WorkflowHistoryEvent.NodeChange:
|
||||
return t('workflow.changeHistory.nodeChange')
|
||||
case WorkflowHistoryEvent.NodeConnect:
|
||||
return t('workflow.changeHistory.nodeConnect')
|
||||
case WorkflowHistoryEvent.NodePaste:
|
||||
return t('workflow.changeHistory.nodePaste')
|
||||
case WorkflowHistoryEvent.NodeDelete:
|
||||
return t('workflow.changeHistory.nodeDelete')
|
||||
case WorkflowHistoryEvent.NodeAdd:
|
||||
return t('workflow.changeHistory.nodeAdd')
|
||||
case WorkflowHistoryEvent.EdgeDelete:
|
||||
case WorkflowHistoryEvent.EdgeDeleteByDeleteBranch:
|
||||
return t('workflow.changeHistory.edgeDelete')
|
||||
case WorkflowHistoryEvent.NodeResize:
|
||||
return t('workflow.changeHistory.nodeResize')
|
||||
case WorkflowHistoryEvent.NoteAdd:
|
||||
return t('workflow.changeHistory.noteAdd')
|
||||
case WorkflowHistoryEvent.NoteChange:
|
||||
return t('workflow.changeHistory.noteChange')
|
||||
case WorkflowHistoryEvent.NoteDelete:
|
||||
return t('workflow.changeHistory.noteDelete')
|
||||
default:
|
||||
return 'Unknown Event'
|
||||
}
|
||||
}, [t])
|
||||
|
||||
return {
|
||||
store: workflowHistoryStore,
|
||||
saveStateToHistory,
|
||||
getHistoryLabel,
|
||||
undo,
|
||||
redo,
|
||||
onUndo,
|
||||
onRedo,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,354 @@
|
||||
import {
|
||||
useCallback,
|
||||
} from 'react'
|
||||
import { useReactFlow, useStoreApi } from 'reactflow'
|
||||
import { produce } from 'immer'
|
||||
import { useStore, useWorkflowStore } from '../store'
|
||||
import {
|
||||
CUSTOM_NODE,
|
||||
NODE_LAYOUT_HORIZONTAL_PADDING,
|
||||
NODE_LAYOUT_VERTICAL_PADDING,
|
||||
WORKFLOW_DATA_UPDATE,
|
||||
} from '../constants'
|
||||
import type { WorkflowDataUpdater } from '../types'
|
||||
import { BlockEnum, ControlMode } from '../types'
|
||||
import {
|
||||
getLayoutByDagre,
|
||||
getLayoutForChildNodes,
|
||||
initialEdges,
|
||||
initialNodes,
|
||||
} from '../utils'
|
||||
import type { LayoutResult } from '../utils'
|
||||
import {
|
||||
useNodesReadOnly,
|
||||
useSelectionInteractions,
|
||||
useWorkflowReadOnly,
|
||||
} from '../hooks'
|
||||
import { useEdgesInteractionsWithoutSync } from './use-edges-interactions-without-sync'
|
||||
import { useNodesInteractionsWithoutSync } from './use-nodes-interactions-without-sync'
|
||||
import { useNodesSyncDraft } from './use-nodes-sync-draft'
|
||||
import { WorkflowHistoryEvent, useWorkflowHistory } from './use-workflow-history'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
|
||||
export const useWorkflowInteractions = () => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const { handleNodeCancelRunningStatus } = useNodesInteractionsWithoutSync()
|
||||
const { handleEdgeCancelRunningStatus } = useEdgesInteractionsWithoutSync()
|
||||
|
||||
const handleCancelDebugAndPreviewPanel = useCallback(() => {
|
||||
workflowStore.setState({
|
||||
showDebugAndPreviewPanel: false,
|
||||
workflowRunningData: undefined,
|
||||
})
|
||||
handleNodeCancelRunningStatus()
|
||||
handleEdgeCancelRunningStatus()
|
||||
}, [workflowStore, handleNodeCancelRunningStatus, handleEdgeCancelRunningStatus])
|
||||
|
||||
return {
|
||||
handleCancelDebugAndPreviewPanel,
|
||||
}
|
||||
}
|
||||
|
||||
export const useWorkflowMoveMode = () => {
|
||||
const setControlMode = useStore(s => s.setControlMode)
|
||||
const {
|
||||
getNodesReadOnly,
|
||||
} = useNodesReadOnly()
|
||||
const { handleSelectionCancel } = useSelectionInteractions()
|
||||
|
||||
const handleModePointer = useCallback(() => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
setControlMode(ControlMode.Pointer)
|
||||
}, [getNodesReadOnly, setControlMode])
|
||||
|
||||
const handleModeHand = useCallback(() => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
setControlMode(ControlMode.Hand)
|
||||
handleSelectionCancel()
|
||||
}, [getNodesReadOnly, setControlMode, handleSelectionCancel])
|
||||
|
||||
return {
|
||||
handleModePointer,
|
||||
handleModeHand,
|
||||
}
|
||||
}
|
||||
|
||||
export const useWorkflowOrganize = () => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const store = useStoreApi()
|
||||
const reactflow = useReactFlow()
|
||||
const { getNodesReadOnly } = useNodesReadOnly()
|
||||
const { saveStateToHistory } = useWorkflowHistory()
|
||||
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
|
||||
const handleLayout = useCallback(async () => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
workflowStore.setState({ nodeAnimation: true })
|
||||
const {
|
||||
getNodes,
|
||||
edges,
|
||||
setNodes,
|
||||
} = store.getState()
|
||||
const { setViewport } = reactflow
|
||||
const nodes = getNodes()
|
||||
|
||||
const loopAndIterationNodes = nodes.filter(
|
||||
node => (node.data.type === BlockEnum.Loop || node.data.type === BlockEnum.Iteration)
|
||||
&& !node.parentId
|
||||
&& node.type === CUSTOM_NODE,
|
||||
)
|
||||
|
||||
const childLayoutEntries = await Promise.all(
|
||||
loopAndIterationNodes.map(async node => [
|
||||
node.id,
|
||||
await getLayoutForChildNodes(node.id, nodes, edges),
|
||||
] as const),
|
||||
)
|
||||
const childLayoutsMap = childLayoutEntries.reduce((acc, [nodeId, layout]) => {
|
||||
if (layout)
|
||||
acc[nodeId] = layout
|
||||
return acc
|
||||
}, {} as Record<string, LayoutResult>)
|
||||
|
||||
const containerSizeChanges: Record<string, { width: number, height: number }> = {}
|
||||
|
||||
loopAndIterationNodes.forEach((parentNode) => {
|
||||
const childLayout = childLayoutsMap[parentNode.id]
|
||||
if (!childLayout) return
|
||||
|
||||
const {
|
||||
bounds,
|
||||
nodes: layoutNodes,
|
||||
} = childLayout
|
||||
|
||||
if (!layoutNodes.size)
|
||||
return
|
||||
|
||||
const requiredWidth = (bounds.maxX - bounds.minX) + NODE_LAYOUT_HORIZONTAL_PADDING * 2
|
||||
const requiredHeight = (bounds.maxY - bounds.minY) + NODE_LAYOUT_VERTICAL_PADDING * 2
|
||||
|
||||
containerSizeChanges[parentNode.id] = {
|
||||
width: Math.max(parentNode.width || 0, requiredWidth),
|
||||
height: Math.max(parentNode.height || 0, requiredHeight),
|
||||
}
|
||||
})
|
||||
|
||||
const nodesWithUpdatedSizes = produce(nodes, (draft) => {
|
||||
draft.forEach((node) => {
|
||||
if ((node.data.type === BlockEnum.Loop || node.data.type === BlockEnum.Iteration)
|
||||
&& containerSizeChanges[node.id]) {
|
||||
node.width = containerSizeChanges[node.id].width
|
||||
node.height = containerSizeChanges[node.id].height
|
||||
|
||||
if (node.data.type === BlockEnum.Loop) {
|
||||
node.data.width = containerSizeChanges[node.id].width
|
||||
node.data.height = containerSizeChanges[node.id].height
|
||||
}
|
||||
else if (node.data.type === BlockEnum.Iteration) {
|
||||
node.data.width = containerSizeChanges[node.id].width
|
||||
node.data.height = containerSizeChanges[node.id].height
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const layout = await getLayoutByDagre(nodesWithUpdatedSizes, edges)
|
||||
|
||||
// Build layer map for vertical alignment - nodes in the same layer should align
|
||||
const layerMap = new Map<number, { minY: number; maxHeight: number }>()
|
||||
layout.nodes.forEach((layoutInfo) => {
|
||||
if (layoutInfo.layer !== undefined) {
|
||||
const existing = layerMap.get(layoutInfo.layer)
|
||||
const newLayerInfo = {
|
||||
minY: existing ? Math.min(existing.minY, layoutInfo.y) : layoutInfo.y,
|
||||
maxHeight: existing ? Math.max(existing.maxHeight, layoutInfo.height) : layoutInfo.height,
|
||||
}
|
||||
layerMap.set(layoutInfo.layer, newLayerInfo)
|
||||
}
|
||||
})
|
||||
|
||||
const newNodes = produce(nodesWithUpdatedSizes, (draft) => {
|
||||
draft.forEach((node) => {
|
||||
if (!node.parentId && node.type === CUSTOM_NODE) {
|
||||
const layoutInfo = layout.nodes.get(node.id)
|
||||
if (!layoutInfo)
|
||||
return
|
||||
|
||||
// Calculate vertical position with layer alignment
|
||||
let yPosition = layoutInfo.y
|
||||
if (layoutInfo.layer !== undefined) {
|
||||
const layerInfo = layerMap.get(layoutInfo.layer)
|
||||
if (layerInfo) {
|
||||
// Align to the center of the tallest node in this layer
|
||||
const layerCenterY = layerInfo.minY + layerInfo.maxHeight / 2
|
||||
yPosition = layerCenterY - layoutInfo.height / 2
|
||||
}
|
||||
}
|
||||
|
||||
node.position = {
|
||||
x: layoutInfo.x,
|
||||
y: yPosition,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
loopAndIterationNodes.forEach((parentNode) => {
|
||||
const childLayout = childLayoutsMap[parentNode.id]
|
||||
if (!childLayout)
|
||||
return
|
||||
|
||||
const childNodes = draft.filter(node => node.parentId === parentNode.id)
|
||||
const {
|
||||
bounds,
|
||||
nodes: layoutNodes,
|
||||
} = childLayout
|
||||
|
||||
childNodes.forEach((childNode) => {
|
||||
const layoutInfo = layoutNodes.get(childNode.id)
|
||||
if (!layoutInfo)
|
||||
return
|
||||
|
||||
childNode.position = {
|
||||
x: NODE_LAYOUT_HORIZONTAL_PADDING + (layoutInfo.x - bounds.minX),
|
||||
y: NODE_LAYOUT_VERTICAL_PADDING + (layoutInfo.y - bounds.minY),
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
setNodes(newNodes)
|
||||
const zoom = 0.7
|
||||
setViewport({
|
||||
x: 0,
|
||||
y: 0,
|
||||
zoom,
|
||||
})
|
||||
saveStateToHistory(WorkflowHistoryEvent.LayoutOrganize)
|
||||
setTimeout(() => {
|
||||
handleSyncWorkflowDraft()
|
||||
})
|
||||
}, [getNodesReadOnly, store, reactflow, workflowStore, handleSyncWorkflowDraft, saveStateToHistory])
|
||||
|
||||
return {
|
||||
handleLayout,
|
||||
}
|
||||
}
|
||||
|
||||
export const useWorkflowZoom = () => {
|
||||
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
const { getWorkflowReadOnly } = useWorkflowReadOnly()
|
||||
const {
|
||||
zoomIn,
|
||||
zoomOut,
|
||||
zoomTo,
|
||||
fitView,
|
||||
} = useReactFlow()
|
||||
|
||||
const handleFitView = useCallback(() => {
|
||||
if (getWorkflowReadOnly())
|
||||
return
|
||||
|
||||
fitView()
|
||||
handleSyncWorkflowDraft()
|
||||
}, [getWorkflowReadOnly, fitView, handleSyncWorkflowDraft])
|
||||
|
||||
const handleBackToOriginalSize = useCallback(() => {
|
||||
if (getWorkflowReadOnly())
|
||||
return
|
||||
|
||||
zoomTo(1)
|
||||
handleSyncWorkflowDraft()
|
||||
}, [getWorkflowReadOnly, zoomTo, handleSyncWorkflowDraft])
|
||||
|
||||
const handleSizeToHalf = useCallback(() => {
|
||||
if (getWorkflowReadOnly())
|
||||
return
|
||||
|
||||
zoomTo(0.5)
|
||||
handleSyncWorkflowDraft()
|
||||
}, [getWorkflowReadOnly, zoomTo, handleSyncWorkflowDraft])
|
||||
|
||||
const handleZoomOut = useCallback(() => {
|
||||
if (getWorkflowReadOnly())
|
||||
return
|
||||
|
||||
zoomOut()
|
||||
handleSyncWorkflowDraft()
|
||||
}, [getWorkflowReadOnly, zoomOut, handleSyncWorkflowDraft])
|
||||
|
||||
const handleZoomIn = useCallback(() => {
|
||||
if (getWorkflowReadOnly())
|
||||
return
|
||||
|
||||
zoomIn()
|
||||
handleSyncWorkflowDraft()
|
||||
}, [getWorkflowReadOnly, zoomIn, handleSyncWorkflowDraft])
|
||||
|
||||
return {
|
||||
handleFitView,
|
||||
handleBackToOriginalSize,
|
||||
handleSizeToHalf,
|
||||
handleZoomOut,
|
||||
handleZoomIn,
|
||||
}
|
||||
}
|
||||
|
||||
export const useWorkflowUpdate = () => {
|
||||
const reactflow = useReactFlow()
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
|
||||
const handleUpdateWorkflowCanvas = useCallback((payload: WorkflowDataUpdater) => {
|
||||
const {
|
||||
nodes,
|
||||
edges,
|
||||
viewport,
|
||||
} = payload
|
||||
const { setViewport } = reactflow
|
||||
eventEmitter?.emit({
|
||||
type: WORKFLOW_DATA_UPDATE,
|
||||
payload: {
|
||||
nodes: initialNodes(nodes, edges),
|
||||
edges: initialEdges(edges, nodes),
|
||||
},
|
||||
} as any)
|
||||
|
||||
// Only set viewport if it exists and is valid
|
||||
if (viewport && typeof viewport.x === 'number' && typeof viewport.y === 'number' && typeof viewport.zoom === 'number')
|
||||
setViewport(viewport)
|
||||
}, [eventEmitter, reactflow])
|
||||
|
||||
return {
|
||||
handleUpdateWorkflowCanvas,
|
||||
}
|
||||
}
|
||||
|
||||
export const useWorkflowCanvasMaximize = () => {
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
|
||||
const maximizeCanvas = useStore(s => s.maximizeCanvas)
|
||||
const setMaximizeCanvas = useStore(s => s.setMaximizeCanvas)
|
||||
const {
|
||||
getNodesReadOnly,
|
||||
} = useNodesReadOnly()
|
||||
|
||||
const handleToggleMaximizeCanvas = useCallback(() => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
setMaximizeCanvas(!maximizeCanvas)
|
||||
localStorage.setItem('workflow-canvas-maximize', String(!maximizeCanvas))
|
||||
eventEmitter?.emit({
|
||||
type: 'workflow-canvas-maximize',
|
||||
payload: !maximizeCanvas,
|
||||
} as any)
|
||||
}, [eventEmitter, getNodesReadOnly, maximizeCanvas, setMaximizeCanvas])
|
||||
|
||||
return {
|
||||
handleToggleMaximizeCanvas,
|
||||
}
|
||||
}
|
||||
14
dify/web/app/components/workflow/hooks/use-workflow-mode.ts
Normal file
14
dify/web/app/components/workflow/hooks/use-workflow-mode.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { useMemo } from 'react'
|
||||
import { useStore } from '../store'
|
||||
|
||||
export const useWorkflowMode = () => {
|
||||
const historyWorkflowData = useStore(s => s.historyWorkflowData)
|
||||
const isRestoring = useStore(s => s.isRestoring)
|
||||
return useMemo(() => {
|
||||
return {
|
||||
normal: !historyWorkflowData && !isRestoring,
|
||||
restoring: isRestoring,
|
||||
viewHistory: !!historyWorkflowData,
|
||||
}
|
||||
}, [historyWorkflowData, isRestoring])
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { useHooksStore } from '@/app/components/workflow/hooks-store'
|
||||
|
||||
export const useWorkflowRefreshDraft = () => {
|
||||
const handleRefreshWorkflowDraft = useHooksStore(s => s.handleRefreshWorkflowDraft)
|
||||
|
||||
return {
|
||||
handleRefreshWorkflowDraft,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
export * from './use-workflow-started'
|
||||
export * from './use-workflow-finished'
|
||||
export * from './use-workflow-failed'
|
||||
export * from './use-workflow-node-started'
|
||||
export * from './use-workflow-node-finished'
|
||||
export * from './use-workflow-node-iteration-started'
|
||||
export * from './use-workflow-node-iteration-next'
|
||||
export * from './use-workflow-node-iteration-finished'
|
||||
export * from './use-workflow-node-loop-started'
|
||||
export * from './use-workflow-node-loop-next'
|
||||
export * from './use-workflow-node-loop-finished'
|
||||
export * from './use-workflow-node-retry'
|
||||
export * from './use-workflow-text-chunk'
|
||||
export * from './use-workflow-text-replace'
|
||||
export * from './use-workflow-agent-log'
|
||||
@@ -0,0 +1,50 @@
|
||||
import { useCallback } from 'react'
|
||||
import { produce } from 'immer'
|
||||
import type { AgentLogResponse } from '@/types/workflow'
|
||||
import { useWorkflowStore } from '@/app/components/workflow/store'
|
||||
|
||||
export const useWorkflowAgentLog = () => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
const handleWorkflowAgentLog = useCallback((params: AgentLogResponse) => {
|
||||
const { data } = params
|
||||
const {
|
||||
workflowRunningData,
|
||||
setWorkflowRunningData,
|
||||
} = workflowStore.getState()
|
||||
|
||||
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
|
||||
const currentIndex = draft.tracing!.findIndex(item => item.node_id === data.node_id)
|
||||
if (currentIndex > -1) {
|
||||
const current = draft.tracing![currentIndex]
|
||||
|
||||
if (current.execution_metadata) {
|
||||
if (current.execution_metadata.agent_log) {
|
||||
const currentLogIndex = current.execution_metadata.agent_log.findIndex(log => log.message_id === data.message_id)
|
||||
if (currentLogIndex > -1) {
|
||||
current.execution_metadata.agent_log[currentLogIndex] = {
|
||||
...current.execution_metadata.agent_log[currentLogIndex],
|
||||
...data,
|
||||
}
|
||||
}
|
||||
else {
|
||||
current.execution_metadata.agent_log.push(data)
|
||||
}
|
||||
}
|
||||
else {
|
||||
current.execution_metadata.agent_log = [data]
|
||||
}
|
||||
}
|
||||
else {
|
||||
current.execution_metadata = {
|
||||
agent_log: [data],
|
||||
} as any
|
||||
}
|
||||
}
|
||||
}))
|
||||
}, [workflowStore])
|
||||
|
||||
return {
|
||||
handleWorkflowAgentLog,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { useCallback } from 'react'
|
||||
import { produce } from 'immer'
|
||||
import { useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import { WorkflowRunningStatus } from '@/app/components/workflow/types'
|
||||
|
||||
export const useWorkflowFailed = () => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
const handleWorkflowFailed = useCallback(() => {
|
||||
const {
|
||||
workflowRunningData,
|
||||
setWorkflowRunningData,
|
||||
} = workflowStore.getState()
|
||||
|
||||
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
|
||||
draft.result = {
|
||||
...draft.result,
|
||||
status: WorkflowRunningStatus.Failed,
|
||||
}
|
||||
}))
|
||||
}, [workflowStore])
|
||||
|
||||
return {
|
||||
handleWorkflowFailed,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { useCallback } from 'react'
|
||||
import { produce } from 'immer'
|
||||
import type { WorkflowFinishedResponse } from '@/types/workflow'
|
||||
import { useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import { getFilesInLogs } from '@/app/components/base/file-uploader/utils'
|
||||
|
||||
export const useWorkflowFinished = () => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
const handleWorkflowFinished = useCallback((params: WorkflowFinishedResponse) => {
|
||||
const { data } = params
|
||||
const {
|
||||
workflowRunningData,
|
||||
setWorkflowRunningData,
|
||||
} = workflowStore.getState()
|
||||
|
||||
const isStringOutput = data.outputs && Object.keys(data.outputs).length === 1 && typeof data.outputs[Object.keys(data.outputs)[0]] === 'string'
|
||||
|
||||
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
|
||||
draft.result = {
|
||||
...draft.result,
|
||||
...data,
|
||||
files: getFilesInLogs(data.outputs),
|
||||
} as any
|
||||
if (isStringOutput) {
|
||||
draft.resultTabActive = true
|
||||
draft.resultText = data.outputs[Object.keys(data.outputs)[0]]
|
||||
}
|
||||
}))
|
||||
}, [workflowStore])
|
||||
|
||||
return {
|
||||
handleWorkflowFinished,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { useCallback } from 'react'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import { produce } from 'immer'
|
||||
import type { NodeFinishedResponse } from '@/types/workflow'
|
||||
import {
|
||||
BlockEnum,
|
||||
NodeRunningStatus,
|
||||
} from '@/app/components/workflow/types'
|
||||
import { ErrorHandleTypeEnum } from '@/app/components/workflow/nodes/_base/components/error-handle/types'
|
||||
import { useWorkflowStore } from '@/app/components/workflow/store'
|
||||
|
||||
export const useWorkflowNodeFinished = () => {
|
||||
const store = useStoreApi()
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
const handleWorkflowNodeFinished = useCallback((params: NodeFinishedResponse) => {
|
||||
const { data } = params
|
||||
const {
|
||||
workflowRunningData,
|
||||
setWorkflowRunningData,
|
||||
} = workflowStore.getState()
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
edges,
|
||||
setEdges,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
|
||||
const currentIndex = draft.tracing!.findIndex(item => item.id === data.id)
|
||||
if (currentIndex > -1) {
|
||||
draft.tracing![currentIndex] = {
|
||||
...draft.tracing![currentIndex],
|
||||
...data,
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
const newNodes = produce(nodes, (draft) => {
|
||||
const currentNode = draft.find(node => node.id === data.node_id)!
|
||||
currentNode.data._runningStatus = data.status
|
||||
if (data.status === NodeRunningStatus.Exception) {
|
||||
if (data.execution_metadata?.error_strategy === ErrorHandleTypeEnum.failBranch)
|
||||
currentNode.data._runningBranchId = ErrorHandleTypeEnum.failBranch
|
||||
}
|
||||
else {
|
||||
if (data.node_type === BlockEnum.IfElse)
|
||||
currentNode.data._runningBranchId = data?.outputs?.selected_case_id
|
||||
|
||||
if (data.node_type === BlockEnum.QuestionClassifier)
|
||||
currentNode.data._runningBranchId = data?.outputs?.class_id
|
||||
}
|
||||
})
|
||||
setNodes(newNodes)
|
||||
const newEdges = produce(edges, (draft) => {
|
||||
const incomeEdges = draft.filter((edge) => {
|
||||
return edge.target === data.node_id
|
||||
})
|
||||
incomeEdges.forEach((edge) => {
|
||||
edge.data = {
|
||||
...edge.data,
|
||||
_targetRunningStatus: data.status,
|
||||
}
|
||||
})
|
||||
})
|
||||
setEdges(newEdges)
|
||||
}, [store, workflowStore])
|
||||
|
||||
return {
|
||||
handleWorkflowNodeFinished,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { useCallback } from 'react'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import { produce } from 'immer'
|
||||
import type { IterationFinishedResponse } from '@/types/workflow'
|
||||
import { useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import { DEFAULT_ITER_TIMES } from '@/app/components/workflow/constants'
|
||||
|
||||
export const useWorkflowNodeIterationFinished = () => {
|
||||
const store = useStoreApi()
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
const handleWorkflowNodeIterationFinished = useCallback((params: IterationFinishedResponse) => {
|
||||
const { data } = params
|
||||
const {
|
||||
workflowRunningData,
|
||||
setWorkflowRunningData,
|
||||
setIterTimes,
|
||||
} = workflowStore.getState()
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
edges,
|
||||
setEdges,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
|
||||
const currentIndex = draft.tracing!.findIndex(item => item.id === data.id)
|
||||
|
||||
if (currentIndex > -1) {
|
||||
draft.tracing![currentIndex] = {
|
||||
...draft.tracing![currentIndex],
|
||||
...data,
|
||||
}
|
||||
}
|
||||
}))
|
||||
setIterTimes(DEFAULT_ITER_TIMES)
|
||||
const newNodes = produce(nodes, (draft) => {
|
||||
const currentNode = draft.find(node => node.id === data.node_id)!
|
||||
|
||||
currentNode.data._runningStatus = data.status
|
||||
})
|
||||
setNodes(newNodes)
|
||||
const newEdges = produce(edges, (draft) => {
|
||||
const incomeEdges = draft.filter((edge) => {
|
||||
return edge.target === data.node_id
|
||||
})
|
||||
incomeEdges.forEach((edge) => {
|
||||
edge.data = {
|
||||
...edge.data,
|
||||
_targetRunningStatus: data.status,
|
||||
}
|
||||
})
|
||||
})
|
||||
setEdges(newEdges)
|
||||
}, [workflowStore, store])
|
||||
|
||||
return {
|
||||
handleWorkflowNodeIterationFinished,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { useCallback } from 'react'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import { produce } from 'immer'
|
||||
import type { IterationNextResponse } from '@/types/workflow'
|
||||
import { useWorkflowStore } from '@/app/components/workflow/store'
|
||||
|
||||
export const useWorkflowNodeIterationNext = () => {
|
||||
const store = useStoreApi()
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
const handleWorkflowNodeIterationNext = useCallback((params: IterationNextResponse) => {
|
||||
const {
|
||||
iterTimes,
|
||||
setIterTimes,
|
||||
} = workflowStore.getState()
|
||||
|
||||
const { data } = params
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
} = store.getState()
|
||||
|
||||
const nodes = getNodes()
|
||||
const newNodes = produce(nodes, (draft) => {
|
||||
const currentNode = draft.find(node => node.id === data.node_id)!
|
||||
currentNode.data._iterationIndex = iterTimes
|
||||
setIterTimes(iterTimes + 1)
|
||||
})
|
||||
setNodes(newNodes)
|
||||
}, [workflowStore, store])
|
||||
|
||||
return {
|
||||
handleWorkflowNodeIterationNext,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import { useCallback } from 'react'
|
||||
import {
|
||||
useReactFlow,
|
||||
useStoreApi,
|
||||
} from 'reactflow'
|
||||
import { produce } from 'immer'
|
||||
import { useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import type { IterationStartedResponse } from '@/types/workflow'
|
||||
import { NodeRunningStatus } from '@/app/components/workflow/types'
|
||||
import { DEFAULT_ITER_TIMES } from '@/app/components/workflow/constants'
|
||||
|
||||
export const useWorkflowNodeIterationStarted = () => {
|
||||
const store = useStoreApi()
|
||||
const reactflow = useReactFlow()
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
const handleWorkflowNodeIterationStarted = useCallback((
|
||||
params: IterationStartedResponse,
|
||||
containerParams: {
|
||||
clientWidth: number,
|
||||
clientHeight: number,
|
||||
},
|
||||
) => {
|
||||
const { data } = params
|
||||
const {
|
||||
workflowRunningData,
|
||||
setWorkflowRunningData,
|
||||
setIterTimes,
|
||||
} = workflowStore.getState()
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
edges,
|
||||
setEdges,
|
||||
transform,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
|
||||
draft.tracing!.push({
|
||||
...data,
|
||||
status: NodeRunningStatus.Running,
|
||||
})
|
||||
}))
|
||||
setIterTimes(DEFAULT_ITER_TIMES)
|
||||
|
||||
const {
|
||||
setViewport,
|
||||
} = reactflow
|
||||
const currentNodeIndex = nodes.findIndex(node => node.id === data.node_id)
|
||||
const currentNode = nodes[currentNodeIndex]
|
||||
const position = currentNode.position
|
||||
const zoom = transform[2]
|
||||
|
||||
if (!currentNode.parentId) {
|
||||
setViewport({
|
||||
x: (containerParams.clientWidth - 400 - currentNode.width! * zoom) / 2 - position.x * zoom,
|
||||
y: (containerParams.clientHeight - currentNode.height! * zoom) / 2 - position.y * zoom,
|
||||
zoom: transform[2],
|
||||
})
|
||||
}
|
||||
const newNodes = produce(nodes, (draft) => {
|
||||
draft[currentNodeIndex].data._runningStatus = NodeRunningStatus.Running
|
||||
draft[currentNodeIndex].data._iterationLength = data.metadata.iterator_length
|
||||
draft[currentNodeIndex].data._waitingRun = false
|
||||
})
|
||||
setNodes(newNodes)
|
||||
const newEdges = produce(edges, (draft) => {
|
||||
const incomeEdges = draft.filter(edge => edge.target === data.node_id)
|
||||
|
||||
incomeEdges.forEach((edge) => {
|
||||
edge.data = {
|
||||
...edge.data,
|
||||
_sourceRunningStatus: nodes.find(node => node.id === edge.source)!.data._runningStatus,
|
||||
_targetRunningStatus: NodeRunningStatus.Running,
|
||||
_waitingRun: false,
|
||||
}
|
||||
})
|
||||
})
|
||||
setEdges(newEdges)
|
||||
}, [workflowStore, store, reactflow])
|
||||
|
||||
return {
|
||||
handleWorkflowNodeIterationStarted,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { useCallback } from 'react'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import { produce } from 'immer'
|
||||
import type { LoopFinishedResponse } from '@/types/workflow'
|
||||
import { useWorkflowStore } from '@/app/components/workflow/store'
|
||||
|
||||
export const useWorkflowNodeLoopFinished = () => {
|
||||
const store = useStoreApi()
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
const handleWorkflowNodeLoopFinished = useCallback((params: LoopFinishedResponse) => {
|
||||
const { data } = params
|
||||
const {
|
||||
workflowRunningData,
|
||||
setWorkflowRunningData,
|
||||
} = workflowStore.getState()
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
edges,
|
||||
setEdges,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
|
||||
const currentIndex = draft.tracing!.findIndex(item => item.id === data.id)
|
||||
|
||||
if (currentIndex > -1) {
|
||||
draft.tracing![currentIndex] = {
|
||||
...draft.tracing![currentIndex],
|
||||
...data,
|
||||
}
|
||||
}
|
||||
}))
|
||||
const newNodes = produce(nodes, (draft) => {
|
||||
const currentNode = draft.find(node => node.id === data.node_id)!
|
||||
|
||||
currentNode.data._runningStatus = data.status
|
||||
})
|
||||
setNodes(newNodes)
|
||||
const newEdges = produce(edges, (draft) => {
|
||||
const incomeEdges = draft.filter((edge) => {
|
||||
return edge.target === data.node_id
|
||||
})
|
||||
incomeEdges.forEach((edge) => {
|
||||
edge.data = {
|
||||
...edge.data,
|
||||
_targetRunningStatus: data.status,
|
||||
}
|
||||
})
|
||||
})
|
||||
setEdges(newEdges)
|
||||
}, [workflowStore, store])
|
||||
|
||||
return {
|
||||
handleWorkflowNodeLoopFinished,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { useCallback } from 'react'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import { produce } from 'immer'
|
||||
import type { LoopNextResponse } from '@/types/workflow'
|
||||
import { NodeRunningStatus } from '@/app/components/workflow/types'
|
||||
|
||||
export const useWorkflowNodeLoopNext = () => {
|
||||
const store = useStoreApi()
|
||||
|
||||
const handleWorkflowNodeLoopNext = useCallback((params: LoopNextResponse) => {
|
||||
const { data } = params
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
} = store.getState()
|
||||
|
||||
const nodes = getNodes()
|
||||
const newNodes = produce(nodes, (draft) => {
|
||||
const currentNode = draft.find(node => node.id === data.node_id)!
|
||||
currentNode.data._loopIndex = data.index
|
||||
|
||||
draft.forEach((node) => {
|
||||
if (node.parentId === data.node_id) {
|
||||
node.data._waitingRun = true
|
||||
node.data._runningStatus = NodeRunningStatus.Waiting
|
||||
}
|
||||
})
|
||||
})
|
||||
setNodes(newNodes)
|
||||
}, [store])
|
||||
|
||||
return {
|
||||
handleWorkflowNodeLoopNext,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { useCallback } from 'react'
|
||||
import {
|
||||
useReactFlow,
|
||||
useStoreApi,
|
||||
} from 'reactflow'
|
||||
import { produce } from 'immer'
|
||||
import { useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import type { LoopStartedResponse } from '@/types/workflow'
|
||||
import { NodeRunningStatus } from '@/app/components/workflow/types'
|
||||
|
||||
export const useWorkflowNodeLoopStarted = () => {
|
||||
const store = useStoreApi()
|
||||
const reactflow = useReactFlow()
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
const handleWorkflowNodeLoopStarted = useCallback((
|
||||
params: LoopStartedResponse,
|
||||
containerParams: {
|
||||
clientWidth: number,
|
||||
clientHeight: number,
|
||||
},
|
||||
) => {
|
||||
const { data } = params
|
||||
const {
|
||||
workflowRunningData,
|
||||
setWorkflowRunningData,
|
||||
} = workflowStore.getState()
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
edges,
|
||||
setEdges,
|
||||
transform,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
|
||||
draft.tracing!.push({
|
||||
...data,
|
||||
status: NodeRunningStatus.Running,
|
||||
})
|
||||
}))
|
||||
|
||||
const {
|
||||
setViewport,
|
||||
} = reactflow
|
||||
const currentNodeIndex = nodes.findIndex(node => node.id === data.node_id)
|
||||
const currentNode = nodes[currentNodeIndex]
|
||||
const position = currentNode.position
|
||||
const zoom = transform[2]
|
||||
|
||||
if (!currentNode.parentId) {
|
||||
setViewport({
|
||||
x: (containerParams.clientWidth - 400 - currentNode.width! * zoom) / 2 - position.x * zoom,
|
||||
y: (containerParams.clientHeight - currentNode.height! * zoom) / 2 - position.y * zoom,
|
||||
zoom: transform[2],
|
||||
})
|
||||
}
|
||||
const newNodes = produce(nodes, (draft) => {
|
||||
draft[currentNodeIndex].data._runningStatus = NodeRunningStatus.Running
|
||||
draft[currentNodeIndex].data._loopLength = data.metadata.loop_length
|
||||
draft[currentNodeIndex].data._waitingRun = false
|
||||
})
|
||||
setNodes(newNodes)
|
||||
const newEdges = produce(edges, (draft) => {
|
||||
const incomeEdges = draft.filter(edge => edge.target === data.node_id)
|
||||
|
||||
incomeEdges.forEach((edge) => {
|
||||
edge.data = {
|
||||
...edge.data,
|
||||
_sourceRunningStatus: nodes.find(node => node.id === edge.source)!.data._runningStatus,
|
||||
_targetRunningStatus: NodeRunningStatus.Running,
|
||||
_waitingRun: false,
|
||||
}
|
||||
})
|
||||
})
|
||||
setEdges(newEdges)
|
||||
}, [workflowStore, store, reactflow])
|
||||
|
||||
return {
|
||||
handleWorkflowNodeLoopStarted,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { useCallback } from 'react'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import { produce } from 'immer'
|
||||
import type {
|
||||
NodeFinishedResponse,
|
||||
} from '@/types/workflow'
|
||||
import { useWorkflowStore } from '@/app/components/workflow/store'
|
||||
|
||||
export const useWorkflowNodeRetry = () => {
|
||||
const store = useStoreApi()
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
const handleWorkflowNodeRetry = useCallback((params: NodeFinishedResponse) => {
|
||||
const { data } = params
|
||||
const {
|
||||
workflowRunningData,
|
||||
setWorkflowRunningData,
|
||||
} = workflowStore.getState()
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
} = store.getState()
|
||||
|
||||
const nodes = getNodes()
|
||||
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
|
||||
draft.tracing!.push(data)
|
||||
}))
|
||||
const newNodes = produce(nodes, (draft) => {
|
||||
const currentNode = draft.find(node => node.id === data.node_id)!
|
||||
|
||||
currentNode.data._retryIndex = data.retry_index
|
||||
})
|
||||
setNodes(newNodes)
|
||||
}, [workflowStore, store])
|
||||
|
||||
return {
|
||||
handleWorkflowNodeRetry,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import { useCallback } from 'react'
|
||||
import {
|
||||
useReactFlow,
|
||||
useStoreApi,
|
||||
} from 'reactflow'
|
||||
import { produce } from 'immer'
|
||||
import type { NodeStartedResponse } from '@/types/workflow'
|
||||
import { NodeRunningStatus } from '@/app/components/workflow/types'
|
||||
import { useWorkflowStore } from '@/app/components/workflow/store'
|
||||
|
||||
export const useWorkflowNodeStarted = () => {
|
||||
const store = useStoreApi()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const reactflow = useReactFlow()
|
||||
|
||||
const handleWorkflowNodeStarted = useCallback((
|
||||
params: NodeStartedResponse,
|
||||
containerParams: {
|
||||
clientWidth: number,
|
||||
clientHeight: number,
|
||||
},
|
||||
) => {
|
||||
const { data } = params
|
||||
const {
|
||||
workflowRunningData,
|
||||
setWorkflowRunningData,
|
||||
} = workflowStore.getState()
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
edges,
|
||||
setEdges,
|
||||
transform,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
|
||||
draft.tracing!.push({
|
||||
...data,
|
||||
status: NodeRunningStatus.Running,
|
||||
})
|
||||
}))
|
||||
|
||||
const {
|
||||
setViewport,
|
||||
} = reactflow
|
||||
const currentNodeIndex = nodes.findIndex(node => node.id === data.node_id)
|
||||
const currentNode = nodes[currentNodeIndex]
|
||||
const position = currentNode.position
|
||||
const zoom = transform[2]
|
||||
|
||||
if (!currentNode.parentId) {
|
||||
setViewport({
|
||||
x: (containerParams.clientWidth - 400 - currentNode.width! * zoom) / 2 - position.x * zoom,
|
||||
y: (containerParams.clientHeight - currentNode.height! * zoom) / 2 - position.y * zoom,
|
||||
zoom: transform[2],
|
||||
})
|
||||
}
|
||||
const newNodes = produce(nodes, (draft) => {
|
||||
draft[currentNodeIndex].data._runningStatus = NodeRunningStatus.Running
|
||||
draft[currentNodeIndex].data._waitingRun = false
|
||||
})
|
||||
setNodes(newNodes)
|
||||
const newEdges = produce(edges, (draft) => {
|
||||
const incomeEdges = draft.filter((edge) => {
|
||||
return edge.target === data.node_id
|
||||
})
|
||||
|
||||
incomeEdges.forEach((edge) => {
|
||||
const incomeNode = nodes.find(node => node.id === edge.source)!
|
||||
if (!incomeNode || !('data' in incomeNode))
|
||||
return
|
||||
|
||||
if (
|
||||
(!incomeNode.data._runningBranchId && edge.sourceHandle === 'source')
|
||||
|| (incomeNode.data._runningBranchId && edge.sourceHandle === incomeNode.data._runningBranchId)
|
||||
) {
|
||||
edge.data = {
|
||||
...edge.data,
|
||||
_sourceRunningStatus: incomeNode.data._runningStatus,
|
||||
_targetRunningStatus: NodeRunningStatus.Running,
|
||||
_waitingRun: false,
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
setEdges(newEdges)
|
||||
}, [workflowStore, store, reactflow])
|
||||
|
||||
return {
|
||||
handleWorkflowNodeStarted,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import {
|
||||
useWorkflowAgentLog,
|
||||
useWorkflowFailed,
|
||||
useWorkflowFinished,
|
||||
useWorkflowNodeFinished,
|
||||
useWorkflowNodeIterationFinished,
|
||||
useWorkflowNodeIterationNext,
|
||||
useWorkflowNodeIterationStarted,
|
||||
useWorkflowNodeLoopFinished,
|
||||
useWorkflowNodeLoopNext,
|
||||
useWorkflowNodeLoopStarted,
|
||||
useWorkflowNodeRetry,
|
||||
useWorkflowNodeStarted,
|
||||
useWorkflowStarted,
|
||||
useWorkflowTextChunk,
|
||||
useWorkflowTextReplace,
|
||||
} from '.'
|
||||
|
||||
export const useWorkflowRunEvent = () => {
|
||||
const { handleWorkflowStarted } = useWorkflowStarted()
|
||||
const { handleWorkflowFinished } = useWorkflowFinished()
|
||||
const { handleWorkflowFailed } = useWorkflowFailed()
|
||||
const { handleWorkflowNodeStarted } = useWorkflowNodeStarted()
|
||||
const { handleWorkflowNodeFinished } = useWorkflowNodeFinished()
|
||||
const { handleWorkflowNodeIterationStarted } = useWorkflowNodeIterationStarted()
|
||||
const { handleWorkflowNodeIterationNext } = useWorkflowNodeIterationNext()
|
||||
const { handleWorkflowNodeIterationFinished } = useWorkflowNodeIterationFinished()
|
||||
const { handleWorkflowNodeLoopStarted } = useWorkflowNodeLoopStarted()
|
||||
const { handleWorkflowNodeLoopNext } = useWorkflowNodeLoopNext()
|
||||
const { handleWorkflowNodeLoopFinished } = useWorkflowNodeLoopFinished()
|
||||
const { handleWorkflowNodeRetry } = useWorkflowNodeRetry()
|
||||
const { handleWorkflowTextChunk } = useWorkflowTextChunk()
|
||||
const { handleWorkflowTextReplace } = useWorkflowTextReplace()
|
||||
const { handleWorkflowAgentLog } = useWorkflowAgentLog()
|
||||
|
||||
return {
|
||||
handleWorkflowStarted,
|
||||
handleWorkflowFinished,
|
||||
handleWorkflowFailed,
|
||||
handleWorkflowNodeStarted,
|
||||
handleWorkflowNodeFinished,
|
||||
handleWorkflowNodeIterationStarted,
|
||||
handleWorkflowNodeIterationNext,
|
||||
handleWorkflowNodeIterationFinished,
|
||||
handleWorkflowNodeLoopStarted,
|
||||
handleWorkflowNodeLoopNext,
|
||||
handleWorkflowNodeLoopFinished,
|
||||
handleWorkflowNodeRetry,
|
||||
handleWorkflowTextChunk,
|
||||
handleWorkflowTextReplace,
|
||||
handleWorkflowAgentLog,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { useCallback } from 'react'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import { produce } from 'immer'
|
||||
import type { WorkflowStartedResponse } from '@/types/workflow'
|
||||
import { WorkflowRunningStatus } from '@/app/components/workflow/types'
|
||||
import { useWorkflowStore } from '@/app/components/workflow/store'
|
||||
|
||||
export const useWorkflowStarted = () => {
|
||||
const store = useStoreApi()
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
const handleWorkflowStarted = useCallback((params: WorkflowStartedResponse) => {
|
||||
const { task_id, data } = params
|
||||
const {
|
||||
workflowRunningData,
|
||||
setWorkflowRunningData,
|
||||
setIterParallelLogMap,
|
||||
} = workflowStore.getState()
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
edges,
|
||||
setEdges,
|
||||
} = store.getState()
|
||||
setIterParallelLogMap(new Map())
|
||||
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
|
||||
draft.task_id = task_id
|
||||
draft.result = {
|
||||
...draft?.result,
|
||||
...data,
|
||||
status: WorkflowRunningStatus.Running,
|
||||
}
|
||||
}))
|
||||
const nodes = getNodes()
|
||||
const newNodes = produce(nodes, (draft) => {
|
||||
draft.forEach((node) => {
|
||||
node.data._waitingRun = true
|
||||
node.data._runningBranchId = undefined
|
||||
})
|
||||
})
|
||||
setNodes(newNodes)
|
||||
const newEdges = produce(edges, (draft) => {
|
||||
draft.forEach((edge) => {
|
||||
edge.data = {
|
||||
...edge.data,
|
||||
_sourceRunningStatus: undefined,
|
||||
_targetRunningStatus: undefined,
|
||||
_waitingRun: true,
|
||||
}
|
||||
})
|
||||
})
|
||||
setEdges(newEdges)
|
||||
}, [workflowStore, store])
|
||||
|
||||
return {
|
||||
handleWorkflowStarted,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { useCallback } from 'react'
|
||||
import { produce } from 'immer'
|
||||
import type { TextChunkResponse } from '@/types/workflow'
|
||||
import { useWorkflowStore } from '@/app/components/workflow/store'
|
||||
|
||||
export const useWorkflowTextChunk = () => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
const handleWorkflowTextChunk = useCallback((params: TextChunkResponse) => {
|
||||
const { data: { text } } = params
|
||||
const {
|
||||
workflowRunningData,
|
||||
setWorkflowRunningData,
|
||||
} = workflowStore.getState()
|
||||
|
||||
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
|
||||
draft.resultTabActive = true
|
||||
draft.resultText += text
|
||||
}))
|
||||
}, [workflowStore])
|
||||
|
||||
return {
|
||||
handleWorkflowTextChunk,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { useCallback } from 'react'
|
||||
import { produce } from 'immer'
|
||||
import type { TextReplaceResponse } from '@/types/workflow'
|
||||
import { useWorkflowStore } from '@/app/components/workflow/store'
|
||||
|
||||
export const useWorkflowTextReplace = () => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
const handleWorkflowTextReplace = useCallback((params: TextReplaceResponse) => {
|
||||
const { data: { text } } = params
|
||||
const {
|
||||
workflowRunningData,
|
||||
setWorkflowRunningData,
|
||||
} = workflowStore.getState()
|
||||
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
|
||||
draft.resultText = text
|
||||
}))
|
||||
}, [workflowStore])
|
||||
|
||||
return {
|
||||
handleWorkflowTextReplace,
|
||||
}
|
||||
}
|
||||
17
dify/web/app/components/workflow/hooks/use-workflow-run.ts
Normal file
17
dify/web/app/components/workflow/hooks/use-workflow-run.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { useHooksStore } from '@/app/components/workflow/hooks-store'
|
||||
|
||||
export const useWorkflowRun = () => {
|
||||
const handleBackupDraft = useHooksStore(s => s.handleBackupDraft)
|
||||
const handleLoadBackupDraft = useHooksStore(s => s.handleLoadBackupDraft)
|
||||
const handleRestoreFromPublishedWorkflow = useHooksStore(s => s.handleRestoreFromPublishedWorkflow)
|
||||
const handleRun = useHooksStore(s => s.handleRun)
|
||||
const handleStopRun = useHooksStore(s => s.handleStopRun)
|
||||
|
||||
return {
|
||||
handleBackupDraft,
|
||||
handleLoadBackupDraft,
|
||||
handleRun,
|
||||
handleStopRun,
|
||||
handleRestoreFromPublishedWorkflow,
|
||||
}
|
||||
}
|
||||
187
dify/web/app/components/workflow/hooks/use-workflow-search.tsx
Normal file
187
dify/web/app/components/workflow/hooks/use-workflow-search.tsx
Normal file
@@ -0,0 +1,187 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useMemo } from 'react'
|
||||
import { useNodes } from 'reactflow'
|
||||
import { useNodesInteractions } from './use-nodes-interactions'
|
||||
import type { CommonNodeType } from '../types'
|
||||
import { workflowNodesAction } from '@/app/components/goto-anything/actions/workflow-nodes'
|
||||
import BlockIcon from '@/app/components/workflow/block-icon'
|
||||
import { setupNodeSelectionListener } from '../utils/node-navigation'
|
||||
import { BlockEnum } from '../types'
|
||||
import type { Emoji } from '@/app/components/tools/types'
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
import { canFindTool } from '@/utils'
|
||||
import type { LLMNodeType } from '../nodes/llm/types'
|
||||
import {
|
||||
useAllBuiltInTools,
|
||||
useAllCustomTools,
|
||||
useAllMCPTools,
|
||||
useAllWorkflowTools,
|
||||
} from '@/service/use-tools'
|
||||
|
||||
/**
|
||||
* Hook to register workflow nodes search functionality
|
||||
*/
|
||||
export const useWorkflowSearch = () => {
|
||||
const nodes = useNodes()
|
||||
const { handleNodeSelect } = useNodesInteractions()
|
||||
|
||||
// Filter and process nodes for search
|
||||
const { data: buildInTools } = useAllBuiltInTools()
|
||||
const { data: customTools } = useAllCustomTools()
|
||||
const { data: workflowTools } = useAllWorkflowTools()
|
||||
const { data: mcpTools } = useAllMCPTools()
|
||||
|
||||
// Extract tool icon logic - clean separation of concerns
|
||||
const getToolIcon = useCallback((nodeData: CommonNodeType): string | Emoji | undefined => {
|
||||
if (nodeData?.type !== BlockEnum.Tool) return undefined
|
||||
|
||||
const toolCollections: Record<string, any[]> = {
|
||||
[CollectionType.builtIn]: buildInTools || [],
|
||||
[CollectionType.custom]: customTools || [],
|
||||
[CollectionType.mcp]: mcpTools || [],
|
||||
}
|
||||
|
||||
const targetTools = (nodeData.provider_type && toolCollections[nodeData.provider_type]) || workflowTools
|
||||
return targetTools?.find((tool: any) => canFindTool(tool.id, nodeData.provider_id))?.icon
|
||||
}, [buildInTools, customTools, workflowTools, mcpTools])
|
||||
|
||||
// Extract model info logic - clean extraction
|
||||
const getModelInfo = useCallback((nodeData: CommonNodeType) => {
|
||||
if (nodeData?.type !== BlockEnum.LLM) return {}
|
||||
|
||||
const llmNodeData = nodeData as LLMNodeType
|
||||
return llmNodeData.model ? {
|
||||
provider: llmNodeData.model.provider,
|
||||
name: llmNodeData.model.name,
|
||||
mode: llmNodeData.model.mode,
|
||||
} : {}
|
||||
}, [])
|
||||
|
||||
const searchableNodes = useMemo(() => {
|
||||
const filteredNodes = nodes.filter((node) => {
|
||||
if (!node.id || !node.data || node.type === 'sticky') return false
|
||||
|
||||
const nodeData = node.data as CommonNodeType
|
||||
const nodeType = nodeData?.type
|
||||
|
||||
const internalStartNodes = ['iteration-start', 'loop-start']
|
||||
return !internalStartNodes.includes(nodeType)
|
||||
})
|
||||
|
||||
return filteredNodes.map((node) => {
|
||||
const nodeData = node.data as CommonNodeType
|
||||
|
||||
return {
|
||||
id: node.id,
|
||||
title: nodeData?.title || nodeData?.type || 'Untitled',
|
||||
type: nodeData?.type || '',
|
||||
desc: nodeData?.desc || '',
|
||||
blockType: nodeData?.type,
|
||||
nodeData,
|
||||
toolIcon: getToolIcon(nodeData),
|
||||
modelInfo: getModelInfo(nodeData),
|
||||
}
|
||||
})
|
||||
}, [nodes, getToolIcon, getModelInfo])
|
||||
|
||||
// Calculate search score - clean scoring logic
|
||||
const calculateScore = useCallback((node: {
|
||||
title: string;
|
||||
type: string;
|
||||
desc: string;
|
||||
modelInfo: { provider?: string; name?: string; mode?: string }
|
||||
}, searchTerm: string): number => {
|
||||
if (!searchTerm) return 1
|
||||
|
||||
const titleMatch = node.title.toLowerCase()
|
||||
const typeMatch = node.type.toLowerCase()
|
||||
const descMatch = node.desc?.toLowerCase() || ''
|
||||
const modelProviderMatch = node.modelInfo?.provider?.toLowerCase() || ''
|
||||
const modelNameMatch = node.modelInfo?.name?.toLowerCase() || ''
|
||||
const modelModeMatch = node.modelInfo?.mode?.toLowerCase() || ''
|
||||
|
||||
let score = 0
|
||||
|
||||
// Title matching (exact prefix > partial match)
|
||||
if (titleMatch.startsWith(searchTerm)) score += 100
|
||||
else if (titleMatch.includes(searchTerm)) score += 50
|
||||
|
||||
// Type matching (exact > partial)
|
||||
if (typeMatch === searchTerm) score += 80
|
||||
else if (typeMatch.includes(searchTerm)) score += 30
|
||||
|
||||
// Description matching (additive)
|
||||
if (descMatch.includes(searchTerm)) score += 20
|
||||
|
||||
// LLM model matching (additive - can combine multiple matches)
|
||||
if (modelNameMatch && modelNameMatch.includes(searchTerm)) score += 60
|
||||
if (modelProviderMatch && modelProviderMatch.includes(searchTerm)) score += 40
|
||||
if (modelModeMatch && modelModeMatch.includes(searchTerm)) score += 30
|
||||
|
||||
return score
|
||||
}, [])
|
||||
|
||||
// Create search function for workflow nodes
|
||||
const searchWorkflowNodes = 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
|
||||
workflowNodesAction.searchFn = searchWorkflowNodes
|
||||
}
|
||||
|
||||
return () => {
|
||||
// Clean up when component unmounts
|
||||
workflowNodesAction.searchFn = undefined
|
||||
}
|
||||
}, [searchableNodes, searchWorkflowNodes])
|
||||
|
||||
// Set up node selection event listener using the utility function
|
||||
useEffect(() => {
|
||||
return setupNodeSelectionListener(handleNodeSelect)
|
||||
}, [handleNodeSelect])
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { useHooksStore } from '@/app/components/workflow/hooks-store'
|
||||
|
||||
export const useWorkflowStartRun = () => {
|
||||
const handleStartWorkflowRun = useHooksStore(s => s.handleStartWorkflowRun)
|
||||
const handleWorkflowStartRunInWorkflow = useHooksStore(s => s.handleWorkflowStartRunInWorkflow)
|
||||
const handleWorkflowStartRunInChatflow = useHooksStore(s => s.handleWorkflowStartRunInChatflow)
|
||||
const handleWorkflowTriggerScheduleRunInWorkflow = useHooksStore(s => s.handleWorkflowTriggerScheduleRunInWorkflow)
|
||||
const handleWorkflowTriggerWebhookRunInWorkflow = useHooksStore(s => s.handleWorkflowTriggerWebhookRunInWorkflow)
|
||||
const handleWorkflowTriggerPluginRunInWorkflow = useHooksStore(s => s.handleWorkflowTriggerPluginRunInWorkflow)
|
||||
const handleWorkflowRunAllTriggersInWorkflow = useHooksStore(s => s.handleWorkflowRunAllTriggersInWorkflow)
|
||||
return {
|
||||
handleStartWorkflowRun,
|
||||
handleWorkflowStartRunInWorkflow,
|
||||
handleWorkflowStartRunInChatflow,
|
||||
handleWorkflowTriggerScheduleRunInWorkflow,
|
||||
handleWorkflowTriggerWebhookRunInWorkflow,
|
||||
handleWorkflowTriggerPluginRunInWorkflow,
|
||||
handleWorkflowRunAllTriggersInWorkflow,
|
||||
}
|
||||
}
|
||||
159
dify/web/app/components/workflow/hooks/use-workflow-variables.ts
Normal file
159
dify/web/app/components/workflow/hooks/use-workflow-variables.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useWorkflowStore } from '../store'
|
||||
import { getVarType, toNodeAvailableVars } from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
||||
import type {
|
||||
Node,
|
||||
NodeOutPutVar,
|
||||
ValueSelector,
|
||||
Var,
|
||||
} from '@/app/components/workflow/types'
|
||||
import { useIsChatMode } from './use-workflow'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import type { Type } from '../nodes/llm/types'
|
||||
import useMatchSchemaType from '../nodes/_base/components/variable/use-match-schema-type'
|
||||
import {
|
||||
useAllBuiltInTools,
|
||||
useAllCustomTools,
|
||||
useAllMCPTools,
|
||||
useAllWorkflowTools,
|
||||
} from '@/service/use-tools'
|
||||
|
||||
export const useWorkflowVariables = () => {
|
||||
const { t } = useTranslation()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const { schemaTypeDefinitions } = useMatchSchemaType()
|
||||
|
||||
const { data: buildInTools } = useAllBuiltInTools()
|
||||
const { data: customTools } = useAllCustomTools()
|
||||
const { data: workflowTools } = useAllWorkflowTools()
|
||||
const { data: mcpTools } = useAllMCPTools()
|
||||
|
||||
const getNodeAvailableVars = useCallback(({
|
||||
parentNode,
|
||||
beforeNodes,
|
||||
isChatMode,
|
||||
filterVar,
|
||||
hideEnv,
|
||||
hideChatVar,
|
||||
}: {
|
||||
parentNode?: Node | null
|
||||
beforeNodes: Node[]
|
||||
isChatMode: boolean
|
||||
filterVar: (payload: Var, selector: ValueSelector) => boolean
|
||||
hideEnv?: boolean
|
||||
hideChatVar?: boolean
|
||||
}): NodeOutPutVar[] => {
|
||||
const {
|
||||
conversationVariables,
|
||||
environmentVariables,
|
||||
ragPipelineVariables,
|
||||
dataSourceList,
|
||||
} = workflowStore.getState()
|
||||
return toNodeAvailableVars({
|
||||
parentNode,
|
||||
t,
|
||||
beforeNodes,
|
||||
isChatMode,
|
||||
environmentVariables: hideEnv ? [] : environmentVariables,
|
||||
conversationVariables: (isChatMode && !hideChatVar) ? conversationVariables : [],
|
||||
ragVariables: ragPipelineVariables,
|
||||
filterVar,
|
||||
allPluginInfoList: {
|
||||
buildInTools: buildInTools || [],
|
||||
customTools: customTools || [],
|
||||
workflowTools: workflowTools || [],
|
||||
mcpTools: mcpTools || [],
|
||||
dataSourceList: dataSourceList || [],
|
||||
},
|
||||
schemaTypeDefinitions,
|
||||
})
|
||||
}, [t, workflowStore, schemaTypeDefinitions, buildInTools, customTools, workflowTools, mcpTools])
|
||||
|
||||
const getCurrentVariableType = useCallback(({
|
||||
parentNode,
|
||||
valueSelector,
|
||||
isIterationItem,
|
||||
isLoopItem,
|
||||
availableNodes,
|
||||
isChatMode,
|
||||
isConstant,
|
||||
preferSchemaType,
|
||||
}: {
|
||||
valueSelector: ValueSelector
|
||||
parentNode?: Node | null
|
||||
isIterationItem?: boolean
|
||||
isLoopItem?: boolean
|
||||
availableNodes: any[]
|
||||
isChatMode: boolean
|
||||
isConstant?: boolean
|
||||
preferSchemaType?: boolean
|
||||
}) => {
|
||||
const {
|
||||
conversationVariables,
|
||||
environmentVariables,
|
||||
ragPipelineVariables,
|
||||
dataSourceList,
|
||||
} = workflowStore.getState()
|
||||
return getVarType({
|
||||
parentNode,
|
||||
valueSelector,
|
||||
isIterationItem,
|
||||
isLoopItem,
|
||||
availableNodes,
|
||||
isChatMode,
|
||||
isConstant,
|
||||
environmentVariables,
|
||||
conversationVariables,
|
||||
ragVariables: ragPipelineVariables,
|
||||
allPluginInfoList: {
|
||||
buildInTools: buildInTools || [],
|
||||
customTools: customTools || [],
|
||||
workflowTools: workflowTools || [],
|
||||
mcpTools: mcpTools || [],
|
||||
dataSourceList: dataSourceList ?? [],
|
||||
},
|
||||
schemaTypeDefinitions,
|
||||
preferSchemaType,
|
||||
})
|
||||
}, [workflowStore, getVarType, schemaTypeDefinitions, buildInTools, customTools, workflowTools, mcpTools])
|
||||
|
||||
return {
|
||||
getNodeAvailableVars,
|
||||
getCurrentVariableType,
|
||||
}
|
||||
}
|
||||
|
||||
export const useWorkflowVariableType = () => {
|
||||
const store = useStoreApi()
|
||||
const {
|
||||
getNodes,
|
||||
} = store.getState()
|
||||
const { getCurrentVariableType } = useWorkflowVariables()
|
||||
|
||||
const isChatMode = useIsChatMode()
|
||||
|
||||
const getVarType = ({
|
||||
nodeId,
|
||||
valueSelector,
|
||||
}: {
|
||||
nodeId: string,
|
||||
valueSelector: ValueSelector,
|
||||
}) => {
|
||||
const node = getNodes().find(n => n.id === nodeId)
|
||||
const isInIteration = !!node?.data.isInIteration
|
||||
const iterationNode = isInIteration ? getNodes().find(n => n.id === node.parentId) : null
|
||||
const availableNodes = [node]
|
||||
|
||||
const type = getCurrentVariableType({
|
||||
parentNode: iterationNode,
|
||||
valueSelector,
|
||||
availableNodes,
|
||||
isChatMode,
|
||||
isConstant: false,
|
||||
})
|
||||
return type as unknown as Type
|
||||
}
|
||||
|
||||
return getVarType
|
||||
}
|
||||
535
dify/web/app/components/workflow/hooks/use-workflow.ts
Normal file
535
dify/web/app/components/workflow/hooks/use-workflow.ts
Normal file
@@ -0,0 +1,535 @@
|
||||
import {
|
||||
useCallback,
|
||||
} from 'react'
|
||||
import { uniqBy } from 'lodash-es'
|
||||
import {
|
||||
getIncomers,
|
||||
getOutgoers,
|
||||
useStoreApi,
|
||||
} from 'reactflow'
|
||||
import type {
|
||||
Connection,
|
||||
} from 'reactflow'
|
||||
import type {
|
||||
BlockEnum,
|
||||
Edge,
|
||||
Node,
|
||||
ValueSelector,
|
||||
} from '../types'
|
||||
import {
|
||||
WorkflowRunningStatus,
|
||||
} from '../types'
|
||||
import {
|
||||
useStore,
|
||||
useWorkflowStore,
|
||||
} from '../store'
|
||||
import {
|
||||
getWorkflowEntryNode,
|
||||
isWorkflowEntryNode,
|
||||
} from '../utils/workflow-entry'
|
||||
import {
|
||||
SUPPORT_OUTPUT_VARS_NODE,
|
||||
} from '../constants'
|
||||
import type { IterationNodeType } from '../nodes/iteration/types'
|
||||
import type { LoopNodeType } from '../nodes/loop/types'
|
||||
import { CUSTOM_NOTE_NODE } from '../note-node/constants'
|
||||
import { findUsedVarNodes, getNodeOutputVars, updateNodeVars } from '../nodes/_base/components/variable/utils'
|
||||
import { useAvailableBlocks } from './use-available-blocks'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
|
||||
import { CUSTOM_ITERATION_START_NODE } from '@/app/components/workflow/nodes/iteration-start/constants'
|
||||
import { CUSTOM_LOOP_START_NODE } from '@/app/components/workflow/nodes/loop-start/constants'
|
||||
import { useNodesMetaData } from '.'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
|
||||
export const useIsChatMode = () => {
|
||||
const appDetail = useAppStore(s => s.appDetail)
|
||||
|
||||
return appDetail?.mode === AppModeEnum.ADVANCED_CHAT
|
||||
}
|
||||
|
||||
export const useWorkflow = () => {
|
||||
const store = useStoreApi()
|
||||
const { getAvailableBlocks } = useAvailableBlocks()
|
||||
const { nodesMap } = useNodesMetaData()
|
||||
|
||||
const getNodeById = useCallback((nodeId: string) => {
|
||||
const {
|
||||
getNodes,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
const currentNode = nodes.find(node => node.id === nodeId)
|
||||
return currentNode
|
||||
}, [store])
|
||||
|
||||
const getTreeLeafNodes = useCallback((nodeId: string) => {
|
||||
const {
|
||||
getNodes,
|
||||
edges,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
// let startNode = getWorkflowEntryNode(nodes)
|
||||
const currentNode = nodes.find(node => node.id === nodeId)
|
||||
|
||||
let startNodes = nodes.filter(node => nodesMap?.[node.data.type as BlockEnum]?.metaData.isStart) || []
|
||||
|
||||
if (currentNode?.parentId) {
|
||||
const startNode = nodes.find(node => node.parentId === currentNode.parentId && (node.type === CUSTOM_ITERATION_START_NODE || node.type === CUSTOM_LOOP_START_NODE))
|
||||
if (startNode)
|
||||
startNodes = [startNode]
|
||||
}
|
||||
|
||||
if (!startNodes.length)
|
||||
return []
|
||||
|
||||
const list: Node[] = []
|
||||
const preOrder = (root: Node, callback: (node: Node) => void) => {
|
||||
if (root.id === nodeId)
|
||||
return
|
||||
const outgoers = getOutgoers(root, nodes, edges)
|
||||
|
||||
if (outgoers.length) {
|
||||
outgoers.forEach((outgoer) => {
|
||||
preOrder(outgoer, callback)
|
||||
})
|
||||
}
|
||||
else {
|
||||
if (root.id !== nodeId)
|
||||
callback(root)
|
||||
}
|
||||
}
|
||||
startNodes.forEach((startNode) => {
|
||||
preOrder(startNode, (node) => {
|
||||
list.push(node)
|
||||
})
|
||||
})
|
||||
|
||||
const incomers = getIncomers({ id: nodeId } as Node, nodes, edges)
|
||||
|
||||
list.push(...incomers)
|
||||
|
||||
return uniqBy(list, 'id').filter((item: Node) => {
|
||||
return SUPPORT_OUTPUT_VARS_NODE.includes(item.data.type)
|
||||
})
|
||||
}, [store, nodesMap])
|
||||
|
||||
const getBeforeNodesInSameBranch = useCallback((nodeId: string, newNodes?: Node[], newEdges?: Edge[]) => {
|
||||
const {
|
||||
getNodes,
|
||||
edges,
|
||||
} = store.getState()
|
||||
const nodes = newNodes || getNodes()
|
||||
const currentNode = nodes.find(node => node.id === nodeId)
|
||||
|
||||
const list: Node[] = []
|
||||
|
||||
if (!currentNode)
|
||||
return list
|
||||
|
||||
if (currentNode.parentId) {
|
||||
const parentNode = nodes.find(node => node.id === currentNode.parentId)
|
||||
if (parentNode) {
|
||||
const parentList = getBeforeNodesInSameBranch(parentNode.id)
|
||||
|
||||
list.push(...parentList)
|
||||
}
|
||||
}
|
||||
|
||||
const traverse = (root: Node, callback: (node: Node) => void) => {
|
||||
if (root) {
|
||||
const incomers = getIncomers(root, nodes, newEdges || edges)
|
||||
|
||||
if (incomers.length) {
|
||||
incomers.forEach((node) => {
|
||||
if (!list.find(n => node.id === n.id)) {
|
||||
callback(node)
|
||||
traverse(node, callback)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
traverse(currentNode, (node) => {
|
||||
list.push(node)
|
||||
})
|
||||
|
||||
const length = list.length
|
||||
if (length) {
|
||||
return uniqBy(list, 'id').reverse().filter((item: Node) => {
|
||||
return SUPPORT_OUTPUT_VARS_NODE.includes(item.data.type)
|
||||
})
|
||||
}
|
||||
|
||||
return []
|
||||
}, [store])
|
||||
|
||||
const getBeforeNodesInSameBranchIncludeParent = useCallback((nodeId: string, newNodes?: Node[], newEdges?: Edge[]) => {
|
||||
const nodes = getBeforeNodesInSameBranch(nodeId, newNodes, newEdges)
|
||||
const {
|
||||
getNodes,
|
||||
} = store.getState()
|
||||
const allNodes = getNodes()
|
||||
const node = allNodes.find(n => n.id === nodeId)
|
||||
const parentNodeId = node?.parentId
|
||||
const parentNode = allNodes.find(n => n.id === parentNodeId)
|
||||
if (parentNode)
|
||||
nodes.push(parentNode)
|
||||
|
||||
return nodes
|
||||
}, [getBeforeNodesInSameBranch, store])
|
||||
|
||||
const getAfterNodesInSameBranch = useCallback((nodeId: string) => {
|
||||
const {
|
||||
getNodes,
|
||||
edges,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
const currentNode = nodes.find(node => node.id === nodeId)!
|
||||
|
||||
if (!currentNode)
|
||||
return []
|
||||
const list: Node[] = [currentNode]
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
traverse(currentNode, (node) => {
|
||||
list.push(node)
|
||||
})
|
||||
|
||||
return uniqBy(list, 'id')
|
||||
}, [store])
|
||||
|
||||
const getBeforeNodeById = useCallback((nodeId: string) => {
|
||||
const {
|
||||
getNodes,
|
||||
edges,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
const node = nodes.find(node => node.id === nodeId)!
|
||||
|
||||
return getIncomers(node, nodes, edges)
|
||||
}, [store])
|
||||
|
||||
const getIterationNodeChildren = useCallback((nodeId: string) => {
|
||||
const {
|
||||
getNodes,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
|
||||
return nodes.filter(node => node.parentId === nodeId)
|
||||
}, [store])
|
||||
|
||||
const getLoopNodeChildren = useCallback((nodeId: string) => {
|
||||
const {
|
||||
getNodes,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
|
||||
return nodes.filter(node => node.parentId === nodeId)
|
||||
}, [store])
|
||||
|
||||
const isFromStartNode = useCallback((nodeId: string) => {
|
||||
const { getNodes } = store.getState()
|
||||
const nodes = getNodes()
|
||||
const currentNode = nodes.find(node => node.id === nodeId)
|
||||
|
||||
if (!currentNode)
|
||||
return false
|
||||
|
||||
if (isWorkflowEntryNode(currentNode.data.type))
|
||||
return true
|
||||
|
||||
const checkPreviousNodes = (node: Node) => {
|
||||
const previousNodes = getBeforeNodeById(node.id)
|
||||
|
||||
for (const prevNode of previousNodes) {
|
||||
if (isWorkflowEntryNode(prevNode.data.type))
|
||||
return true
|
||||
if (checkPreviousNodes(prevNode))
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return checkPreviousNodes(currentNode)
|
||||
}, [store, getBeforeNodeById])
|
||||
|
||||
const handleOutVarRenameChange = useCallback((nodeId: string, oldValeSelector: ValueSelector, newVarSelector: ValueSelector) => {
|
||||
const { getNodes, setNodes } = store.getState()
|
||||
const allNodes = getNodes()
|
||||
const affectedNodes = findUsedVarNodes(oldValeSelector, allNodes)
|
||||
if (affectedNodes.length > 0) {
|
||||
const newNodes = allNodes.map((node) => {
|
||||
if (affectedNodes.find(n => n.id === node.id))
|
||||
return updateNodeVars(node, oldValeSelector, newVarSelector)
|
||||
|
||||
return node
|
||||
})
|
||||
setNodes(newNodes)
|
||||
}
|
||||
}, [store])
|
||||
|
||||
const isVarUsedInNodes = useCallback((varSelector: ValueSelector) => {
|
||||
const nodeId = varSelector[0]
|
||||
const afterNodes = getAfterNodesInSameBranch(nodeId)
|
||||
const effectNodes = findUsedVarNodes(varSelector, afterNodes)
|
||||
return effectNodes.length > 0
|
||||
}, [getAfterNodesInSameBranch])
|
||||
|
||||
const removeUsedVarInNodes = useCallback((varSelector: ValueSelector) => {
|
||||
const nodeId = varSelector[0]
|
||||
const { getNodes, setNodes } = store.getState()
|
||||
const afterNodes = getAfterNodesInSameBranch(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)
|
||||
}
|
||||
}, [getAfterNodesInSameBranch, store])
|
||||
|
||||
const isNodeVarsUsedInNodes = useCallback((node: Node, isChatMode: boolean) => {
|
||||
const outputVars = getNodeOutputVars(node, isChatMode)
|
||||
const isUsed = outputVars.some((varSelector) => {
|
||||
return isVarUsedInNodes(varSelector)
|
||||
})
|
||||
return isUsed
|
||||
}, [isVarUsedInNodes])
|
||||
|
||||
const getRootNodesById = useCallback((nodeId: string) => {
|
||||
const {
|
||||
getNodes,
|
||||
edges,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
const currentNode = nodes.find(node => node.id === nodeId)
|
||||
|
||||
const rootNodes: Node[] = []
|
||||
|
||||
if (!currentNode)
|
||||
return rootNodes
|
||||
|
||||
if (currentNode.parentId) {
|
||||
const parentNode = nodes.find(node => node.id === currentNode.parentId)
|
||||
if (parentNode) {
|
||||
const parentList = getRootNodesById(parentNode.id)
|
||||
|
||||
rootNodes.push(...parentList)
|
||||
}
|
||||
}
|
||||
|
||||
const traverse = (root: Node, callback: (node: Node) => void) => {
|
||||
if (root) {
|
||||
const incomers = getIncomers(root, nodes, edges)
|
||||
|
||||
if (incomers.length) {
|
||||
incomers.forEach((node) => {
|
||||
traverse(node, callback)
|
||||
})
|
||||
}
|
||||
else {
|
||||
callback(root)
|
||||
}
|
||||
}
|
||||
}
|
||||
traverse(currentNode, (node) => {
|
||||
rootNodes.push(node)
|
||||
})
|
||||
|
||||
const length = rootNodes.length
|
||||
if (length)
|
||||
return uniqBy(rootNodes, 'id')
|
||||
|
||||
return []
|
||||
}, [store])
|
||||
|
||||
const getStartNodes = useCallback((nodes: Node[], currentNode?: Node) => {
|
||||
const { id, parentId } = currentNode || {}
|
||||
let startNodes: Node[] = []
|
||||
|
||||
if (parentId) {
|
||||
const parentNode = nodes.find(node => node.id === parentId)
|
||||
if (!parentNode)
|
||||
throw new Error('Parent node not found')
|
||||
|
||||
const startNode = nodes.find(node => node.id === (parentNode.data as (IterationNodeType | LoopNodeType)).start_node_id)
|
||||
if (startNode)
|
||||
startNodes = [startNode]
|
||||
}
|
||||
else {
|
||||
startNodes = nodes.filter(node => nodesMap?.[node.data.type as BlockEnum]?.metaData.isStart) || []
|
||||
}
|
||||
|
||||
if (!startNodes.length)
|
||||
startNodes = getRootNodesById(id || '')
|
||||
|
||||
return startNodes
|
||||
}, [nodesMap, getRootNodesById])
|
||||
|
||||
const isValidConnection = useCallback(({ source, sourceHandle: _sourceHandle, target }: Connection) => {
|
||||
const {
|
||||
edges,
|
||||
getNodes,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
const sourceNode: Node = nodes.find(node => node.id === source)!
|
||||
const targetNode: Node = nodes.find(node => node.id === target)!
|
||||
|
||||
if (sourceNode.type === CUSTOM_NOTE_NODE || targetNode.type === CUSTOM_NOTE_NODE)
|
||||
return false
|
||||
|
||||
if (sourceNode.parentId !== targetNode.parentId)
|
||||
return false
|
||||
|
||||
if (sourceNode && targetNode) {
|
||||
const sourceNodeAvailableNextNodes = getAvailableBlocks(sourceNode.data.type, !!sourceNode.parentId).availableNextBlocks
|
||||
const targetNodeAvailablePrevNodes = getAvailableBlocks(targetNode.data.type, !!targetNode.parentId).availablePrevBlocks
|
||||
|
||||
if (!sourceNodeAvailableNextNodes.includes(targetNode.data.type))
|
||||
return false
|
||||
|
||||
if (!targetNodeAvailablePrevNodes.includes(sourceNode.data.type))
|
||||
return false
|
||||
}
|
||||
|
||||
const hasCycle = (node: Node, visited = new Set()) => {
|
||||
if (visited.has(node.id))
|
||||
return false
|
||||
|
||||
visited.add(node.id)
|
||||
|
||||
for (const outgoer of getOutgoers(node, nodes, edges)) {
|
||||
if (outgoer.id === source)
|
||||
return true
|
||||
if (hasCycle(outgoer, visited))
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return !hasCycle(targetNode)
|
||||
}, [store, getAvailableBlocks])
|
||||
|
||||
const getNode = useCallback((nodeId?: string) => {
|
||||
const { getNodes } = store.getState()
|
||||
const nodes = getNodes()
|
||||
|
||||
return nodes.find(node => node.id === nodeId) || getWorkflowEntryNode(nodes)
|
||||
}, [store])
|
||||
|
||||
return {
|
||||
getNodeById,
|
||||
getTreeLeafNodes,
|
||||
getBeforeNodesInSameBranch,
|
||||
getBeforeNodesInSameBranchIncludeParent,
|
||||
getAfterNodesInSameBranch,
|
||||
handleOutVarRenameChange,
|
||||
isVarUsedInNodes,
|
||||
removeUsedVarInNodes,
|
||||
isNodeVarsUsedInNodes,
|
||||
isValidConnection,
|
||||
getBeforeNodeById,
|
||||
getIterationNodeChildren,
|
||||
getLoopNodeChildren,
|
||||
getRootNodesById,
|
||||
getStartNodes,
|
||||
isFromStartNode,
|
||||
getNode,
|
||||
}
|
||||
}
|
||||
|
||||
export const useWorkflowReadOnly = () => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const workflowRunningData = useStore(s => s.workflowRunningData)
|
||||
|
||||
const getWorkflowReadOnly = useCallback(() => {
|
||||
return workflowStore.getState().workflowRunningData?.result.status === WorkflowRunningStatus.Running
|
||||
}, [workflowStore])
|
||||
|
||||
return {
|
||||
workflowReadOnly: workflowRunningData?.result.status === WorkflowRunningStatus.Running,
|
||||
getWorkflowReadOnly,
|
||||
}
|
||||
}
|
||||
|
||||
export const useNodesReadOnly = () => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
const workflowRunningData = useStore(s => s.workflowRunningData)
|
||||
const historyWorkflowData = useStore(s => s.historyWorkflowData)
|
||||
const isRestoring = useStore(s => s.isRestoring)
|
||||
|
||||
const getNodesReadOnly = useCallback((): boolean => {
|
||||
const {
|
||||
workflowRunningData,
|
||||
historyWorkflowData,
|
||||
isRestoring,
|
||||
} = workflowStore.getState()
|
||||
|
||||
return !!(workflowRunningData?.result.status === WorkflowRunningStatus.Running || historyWorkflowData || isRestoring)
|
||||
}, [workflowStore])
|
||||
|
||||
return {
|
||||
nodesReadOnly: !!(workflowRunningData?.result.status === WorkflowRunningStatus.Running || historyWorkflowData || isRestoring),
|
||||
getNodesReadOnly,
|
||||
}
|
||||
}
|
||||
|
||||
export const useIsNodeInIteration = (iterationId: string) => {
|
||||
const store = useStoreApi()
|
||||
|
||||
const isNodeInIteration = useCallback((nodeId: string) => {
|
||||
const {
|
||||
getNodes,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
const node = nodes.find(node => node.id === nodeId)
|
||||
|
||||
if (!node)
|
||||
return false
|
||||
|
||||
if (node.parentId === iterationId)
|
||||
return true
|
||||
|
||||
return false
|
||||
}, [iterationId, store])
|
||||
return {
|
||||
isNodeInIteration,
|
||||
}
|
||||
}
|
||||
|
||||
export const useIsNodeInLoop = (loopId: string) => {
|
||||
const store = useStoreApi()
|
||||
|
||||
const isNodeInLoop = useCallback((nodeId: string) => {
|
||||
const {
|
||||
getNodes,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
const node = nodes.find(node => node.id === nodeId)
|
||||
|
||||
if (!node)
|
||||
return false
|
||||
|
||||
if (node.parentId === loopId)
|
||||
return true
|
||||
|
||||
return false
|
||||
}, [loopId, store])
|
||||
return {
|
||||
isNodeInLoop,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user