Spaces:
Paused
Paused
| import { | |
| useCallback, | |
| useEffect, | |
| useMemo, | |
| useState, | |
| } from 'react' | |
| import dayjs from 'dayjs' | |
| import { uniqBy } from 'lodash-es' | |
| import { useTranslation } from 'react-i18next' | |
| import { useContext } from 'use-context-selector' | |
| import { | |
| getIncomers, | |
| getOutgoers, | |
| useStoreApi, | |
| } from 'reactflow' | |
| import type { | |
| Connection, | |
| } from 'reactflow' | |
| import type { | |
| Edge, | |
| Node, | |
| ValueSelector, | |
| } from '../types' | |
| import { | |
| BlockEnum, | |
| WorkflowRunningStatus, | |
| } from '../types' | |
| import { | |
| useStore, | |
| useWorkflowStore, | |
| } from '../store' | |
| import { | |
| getParallelInfo, | |
| } from '../utils' | |
| import { | |
| PARALLEL_DEPTH_LIMIT, | |
| PARALLEL_LIMIT, | |
| SUPPORT_OUTPUT_VARS_NODE, | |
| } from '../constants' | |
| import { CUSTOM_NOTE_NODE } from '../note-node/constants' | |
| import { findUsedVarNodes, getNodeOutputVars, updateNodeVars } from '../nodes/_base/components/variable/utils' | |
| import { useNodesExtraData } from './use-nodes-data' | |
| import { useWorkflowTemplate } from './use-workflow-template' | |
| import { useStore as useAppStore } from '@/app/components/app/store' | |
| import { | |
| fetchNodesDefaultConfigs, | |
| fetchPublishedWorkflow, | |
| fetchWorkflowDraft, | |
| syncWorkflowDraft, | |
| } from '@/service/workflow' | |
| import type { FetchWorkflowDraftResponse } from '@/types/workflow' | |
| import { | |
| fetchAllBuiltInTools, | |
| fetchAllCustomTools, | |
| fetchAllWorkflowTools, | |
| } from '@/service/tools' | |
| import I18n from '@/context/i18n' | |
| import { CollectionType } from '@/app/components/tools/types' | |
| import { CUSTOM_ITERATION_START_NODE } from '@/app/components/workflow/nodes/iteration-start/constants' | |
| export const useIsChatMode = () => { | |
| const appDetail = useAppStore(s => s.appDetail) | |
| return appDetail?.mode === 'advanced-chat' | |
| } | |
| export const useWorkflow = () => { | |
| const { t } = useTranslation() | |
| const { locale } = useContext(I18n) | |
| const store = useStoreApi() | |
| const workflowStore = useWorkflowStore() | |
| const nodesExtraData = useNodesExtraData() | |
| const setPanelWidth = useCallback((width: number) => { | |
| localStorage.setItem('workflow-node-panel-width', `${width}`) | |
| workflowStore.setState({ panelWidth: width }) | |
| }, [workflowStore]) | |
| const getTreeLeafNodes = useCallback((nodeId: string) => { | |
| const { | |
| getNodes, | |
| edges, | |
| } = store.getState() | |
| const nodes = getNodes() | |
| let startNode = nodes.find(node => node.data.type === BlockEnum.Start) | |
| const currentNode = nodes.find(node => node.id === nodeId) | |
| if (currentNode?.parentId) | |
| startNode = nodes.find(node => node.parentId === currentNode.parentId && node.type === CUSTOM_ITERATION_START_NODE) | |
| if (!startNode) | |
| 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) | |
| } | |
| } | |
| preOrder(startNode, (node) => { | |
| list.push(node) | |
| }) | |
| const incomers = getIncomers({ id: nodeId } as Node, nodes, edges) | |
| list.push(...incomers) | |
| return uniqBy(list, 'id').filter((item) => { | |
| return SUPPORT_OUTPUT_VARS_NODE.includes(item.data.type) | |
| }) | |
| }, [store]) | |
| 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) => { | |
| 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 isFromStartNode = useCallback((nodeId: string) => { | |
| const { getNodes } = store.getState() | |
| const nodes = getNodes() | |
| const currentNode = nodes.find(node => node.id === nodeId) | |
| if (!currentNode) | |
| return false | |
| if (currentNode.data.type === BlockEnum.Start) | |
| return true | |
| const checkPreviousNodes = (node: Node) => { | |
| const previousNodes = getBeforeNodeById(node.id) | |
| for (const prevNode of previousNodes) { | |
| if (prevNode.data.type === BlockEnum.Start) | |
| 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 afterNodes = getAfterNodesInSameBranch(nodeId) | |
| const effectNodes = findUsedVarNodes(oldValeSelector, afterNodes) | |
| if (effectNodes.length > 0) { | |
| const newNodes = getNodes().map((node) => { | |
| if (effectNodes.find(n => n.id === node.id)) | |
| return updateNodeVars(node, oldValeSelector, newVarSelector) | |
| return node | |
| }) | |
| setNodes(newNodes) | |
| } | |
| // eslint-disable-next-line react-hooks/exhaustive-deps | |
| }, [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 checkParallelLimit = useCallback((nodeId: string, nodeHandle = 'source') => { | |
| const { | |
| edges, | |
| } = store.getState() | |
| const connectedEdges = edges.filter(edge => edge.source === nodeId && edge.sourceHandle === nodeHandle) | |
| if (connectedEdges.length > PARALLEL_LIMIT - 1) { | |
| const { setShowTips } = workflowStore.getState() | |
| setShowTips(t('workflow.common.parallelTip.limit', { num: PARALLEL_LIMIT })) | |
| return false | |
| } | |
| return true | |
| }, [store, workflowStore, t]) | |
| const checkNestedParallelLimit = useCallback((nodes: Node[], edges: Edge[], parentNodeId?: string) => { | |
| const { | |
| parallelList, | |
| hasAbnormalEdges, | |
| } = getParallelInfo(nodes, edges, parentNodeId) | |
| if (hasAbnormalEdges) | |
| return false | |
| for (let i = 0; i < parallelList.length; i++) { | |
| const parallel = parallelList[i] | |
| if (parallel.depth > PARALLEL_DEPTH_LIMIT) { | |
| const { setShowTips } = workflowStore.getState() | |
| setShowTips(t('workflow.common.parallelTip.depthLimit', { num: PARALLEL_DEPTH_LIMIT })) | |
| return false | |
| } | |
| } | |
| return true | |
| }, [t, workflowStore]) | |
| const isValidConnection = useCallback(({ source, 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 (!checkParallelLimit(source!, sourceHandle || 'source')) | |
| return false | |
| 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 = nodesExtraData[sourceNode.data.type].availableNextNodes | |
| const targetNodeAvailablePrevNodes = [...nodesExtraData[targetNode.data.type].availablePrevNodes, BlockEnum.Start] | |
| 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, nodesExtraData, checkParallelLimit]) | |
| const formatTimeFromNow = useCallback((time: number) => { | |
| return dayjs(time).locale(locale === 'zh-Hans' ? 'zh-cn' : locale).fromNow() | |
| }, [locale]) | |
| const getNode = useCallback((nodeId?: string) => { | |
| const { getNodes } = store.getState() | |
| const nodes = getNodes() | |
| return nodes.find(node => node.id === nodeId) || nodes.find(node => node.data.type === BlockEnum.Start) | |
| }, [store]) | |
| return { | |
| setPanelWidth, | |
| getTreeLeafNodes, | |
| getBeforeNodesInSameBranch, | |
| getBeforeNodesInSameBranchIncludeParent, | |
| getAfterNodesInSameBranch, | |
| handleOutVarRenameChange, | |
| isVarUsedInNodes, | |
| removeUsedVarInNodes, | |
| isNodeVarsUsedInNodes, | |
| checkParallelLimit, | |
| checkNestedParallelLimit, | |
| isValidConnection, | |
| isFromStartNode, | |
| formatTimeFromNow, | |
| getNode, | |
| getBeforeNodeById, | |
| getIterationNodeChildren, | |
| } | |
| } | |
| export const useFetchToolsData = () => { | |
| const workflowStore = useWorkflowStore() | |
| const handleFetchAllTools = useCallback(async (type: string) => { | |
| if (type === 'builtin') { | |
| const buildInTools = await fetchAllBuiltInTools() | |
| workflowStore.setState({ | |
| buildInTools: buildInTools || [], | |
| }) | |
| } | |
| if (type === 'custom') { | |
| const customTools = await fetchAllCustomTools() | |
| workflowStore.setState({ | |
| customTools: customTools || [], | |
| }) | |
| } | |
| if (type === 'workflow') { | |
| const workflowTools = await fetchAllWorkflowTools() | |
| workflowStore.setState({ | |
| workflowTools: workflowTools || [], | |
| }) | |
| } | |
| }, [workflowStore]) | |
| return { | |
| handleFetchAllTools, | |
| } | |
| } | |
| export const useWorkflowInit = () => { | |
| const workflowStore = useWorkflowStore() | |
| const { | |
| nodes: nodesTemplate, | |
| edges: edgesTemplate, | |
| } = useWorkflowTemplate() | |
| const { handleFetchAllTools } = useFetchToolsData() | |
| const appDetail = useAppStore(state => state.appDetail)! | |
| const setSyncWorkflowDraftHash = useStore(s => s.setSyncWorkflowDraftHash) | |
| const [data, setData] = useState<FetchWorkflowDraftResponse>() | |
| const [isLoading, setIsLoading] = useState(true) | |
| useEffect(() => { | |
| workflowStore.setState({ appId: appDetail.id }) | |
| }, [appDetail.id, workflowStore]) | |
| const handleGetInitialWorkflowData = useCallback(async () => { | |
| try { | |
| const res = await fetchWorkflowDraft(`/apps/${appDetail.id}/workflows/draft`) | |
| setData(res) | |
| workflowStore.setState({ | |
| envSecrets: (res.environment_variables || []).filter(env => env.value_type === 'secret').reduce((acc, env) => { | |
| acc[env.id] = env.value | |
| return acc | |
| }, {} as Record<string, string>), | |
| environmentVariables: res.environment_variables?.map(env => env.value_type === 'secret' ? { ...env, value: '[__HIDDEN__]' } : env) || [], | |
| // #TODO chatVar sync# | |
| conversationVariables: res.conversation_variables || [], | |
| }) | |
| setSyncWorkflowDraftHash(res.hash) | |
| setIsLoading(false) | |
| } | |
| catch (error: any) { | |
| if (error && error.json && !error.bodyUsed && appDetail) { | |
| error.json().then((err: any) => { | |
| if (err.code === 'draft_workflow_not_exist') { | |
| workflowStore.setState({ notInitialWorkflow: true }) | |
| syncWorkflowDraft({ | |
| url: `/apps/${appDetail.id}/workflows/draft`, | |
| params: { | |
| graph: { | |
| nodes: nodesTemplate, | |
| edges: edgesTemplate, | |
| }, | |
| features: { | |
| retriever_resource: { enabled: true }, | |
| }, | |
| environment_variables: [], | |
| conversation_variables: [], | |
| }, | |
| }).then((res) => { | |
| workflowStore.getState().setDraftUpdatedAt(res.updated_at) | |
| handleGetInitialWorkflowData() | |
| }) | |
| } | |
| }) | |
| } | |
| } | |
| }, [appDetail, nodesTemplate, edgesTemplate, workflowStore, setSyncWorkflowDraftHash]) | |
| useEffect(() => { | |
| handleGetInitialWorkflowData() | |
| // eslint-disable-next-line react-hooks/exhaustive-deps | |
| }, []) | |
| const handleFetchPreloadData = useCallback(async () => { | |
| try { | |
| const nodesDefaultConfigsData = await fetchNodesDefaultConfigs(`/apps/${appDetail?.id}/workflows/default-workflow-block-configs`) | |
| const publishedWorkflow = await fetchPublishedWorkflow(`/apps/${appDetail?.id}/workflows/publish`) | |
| workflowStore.setState({ | |
| nodesDefaultConfigs: nodesDefaultConfigsData.reduce((acc, block) => { | |
| if (!acc[block.type]) | |
| acc[block.type] = { ...block.config } | |
| return acc | |
| }, {} as Record<string, any>), | |
| }) | |
| workflowStore.getState().setPublishedAt(publishedWorkflow?.created_at) | |
| } | |
| catch (e) { | |
| } | |
| }, [workflowStore, appDetail]) | |
| useEffect(() => { | |
| handleFetchPreloadData() | |
| handleFetchAllTools('builtin') | |
| handleFetchAllTools('custom') | |
| handleFetchAllTools('workflow') | |
| }, [handleFetchPreloadData, handleFetchAllTools]) | |
| useEffect(() => { | |
| if (data) { | |
| workflowStore.getState().setDraftUpdatedAt(data.updated_at) | |
| workflowStore.getState().setToolPublished(data.tool_published) | |
| } | |
| }, [data, workflowStore]) | |
| return { | |
| data, | |
| isLoading, | |
| } | |
| } | |
| 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(() => { | |
| 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 useToolIcon = (data: Node['data']) => { | |
| const buildInTools = useStore(s => s.buildInTools) | |
| const customTools = useStore(s => s.customTools) | |
| const workflowTools = useStore(s => s.workflowTools) | |
| const toolIcon = useMemo(() => { | |
| if (data.type === BlockEnum.Tool) { | |
| let targetTools = buildInTools | |
| if (data.provider_type === CollectionType.builtIn) | |
| targetTools = buildInTools | |
| else if (data.provider_type === CollectionType.custom) | |
| targetTools = customTools | |
| else | |
| targetTools = workflowTools | |
| return targetTools.find(toolWithProvider => toolWithProvider.id === data.provider_id)?.icon | |
| } | |
| }, [data, buildInTools, customTools, workflowTools]) | |
| return toolIcon | |
| } | |
| 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, | |
| } | |
| } | |