Spaces:
Running
Running
import { | |
useCallback, | |
useEffect, | |
useMemo, | |
useState, | |
} from 'react' | |
import dayjs from 'dayjs' | |
import { uniqBy } from 'lodash-es' | |
import { useContext } from 'use-context-selector' | |
import produce from 'immer' | |
import { | |
getIncomers, | |
getOutgoers, | |
useReactFlow, | |
useStoreApi, | |
} from 'reactflow' | |
import type { | |
Connection, | |
} from 'reactflow' | |
import { | |
getLayoutByDagre, | |
} from '../utils' | |
import type { | |
Node, | |
ValueSelector, | |
} from '../types' | |
import { | |
BlockEnum, | |
WorkflowRunningStatus, | |
} from '../types' | |
import { | |
useStore, | |
useWorkflowStore, | |
} from '../store' | |
import { | |
AUTO_LAYOUT_OFFSET, | |
SUPPORT_OUTPUT_VARS_NODE, | |
} from '../constants' | |
import { findUsedVarNodes, getNodeOutputVars, updateNodeVars } from '../nodes/_base/components/variable/utils' | |
import { useNodesExtraData } from './use-nodes-data' | |
import { useWorkflowTemplate } from './use-workflow-template' | |
import { useNodesSyncDraft } from './use-nodes-sync-draft' | |
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, | |
} from '@/service/tools' | |
import I18n from '@/context/i18n' | |
export const useIsChatMode = () => { | |
const appDetail = useAppStore(s => s.appDetail) | |
return appDetail?.mode === 'advanced-chat' | |
} | |
export const useWorkflow = () => { | |
const { locale } = useContext(I18n) | |
const store = useStoreApi() | |
const reactflow = useReactFlow() | |
const workflowStore = useWorkflowStore() | |
const nodesExtraData = useNodesExtraData() | |
const { handleSyncWorkflowDraft } = useNodesSyncDraft() | |
const setPanelWidth = useCallback((width: number) => { | |
localStorage.setItem('workflow-node-panel-width', `${width}`) | |
workflowStore.setState({ panelWidth: width }) | |
}, [workflowStore]) | |
const handleLayout = useCallback(async () => { | |
workflowStore.setState({ nodeAnimation: true }) | |
const { | |
getNodes, | |
edges, | |
setNodes, | |
} = store.getState() | |
const { setViewport } = reactflow | |
const nodes = getNodes() | |
const layout = getLayoutByDagre(nodes, edges) | |
const newNodes = produce(nodes, (draft) => { | |
draft.forEach((node) => { | |
const nodeWithPosition = layout.node(node.id) | |
node.position = { | |
x: nodeWithPosition.x + AUTO_LAYOUT_OFFSET.x, | |
y: nodeWithPosition.y + AUTO_LAYOUT_OFFSET.y, | |
} | |
}) | |
}) | |
setNodes(newNodes) | |
const zoom = 0.7 | |
setViewport({ | |
x: 0, | |
y: 0, | |
zoom, | |
}) | |
setTimeout(() => { | |
handleSyncWorkflowDraft() | |
}) | |
}, [store, reactflow, handleSyncWorkflowDraft, workflowStore]) | |
const getTreeLeafNodes = useCallback((nodeId: string) => { | |
const { | |
getNodes, | |
edges, | |
} = store.getState() | |
const nodes = getNodes() | |
const startNode = nodes.find(node => node.data.type === BlockEnum.Start) | |
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) => { | |
const { | |
getNodes, | |
edges, | |
} = store.getState() | |
const nodes = getNodes() | |
const currentNode = nodes.find(node => node.id === nodeId) | |
const list: Node[] = [] | |
if (!currentNode) | |
return list | |
const traverse = (root: Node, callback: (node: Node) => void) => { | |
if (root) { | |
const incomers = getIncomers(root, nodes, 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 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 handleOutVarRenameChange = useCallback((nodeId: string, oldValeSelector: ValueSelector, newVarSelector: ValueSelector) => { | |
const { getNodes, setNodes } = store.getState() | |
const afterNodes = getAfterNodesInSameBranch(nodeId) | |
const effectNodes = findUsedVarNodes(oldValeSelector, afterNodes) | |
// console.log(effectNodes) | |
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 isValidConnection = useCallback(({ source, 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 && 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]) | |
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]) | |
const enableShortcuts = useCallback(() => { | |
const { setShortcutsDisabled } = workflowStore.getState() | |
setShortcutsDisabled(false) | |
}, [workflowStore]) | |
const disableShortcuts = useCallback(() => { | |
const { setShortcutsDisabled } = workflowStore.getState() | |
setShortcutsDisabled(true) | |
}, [workflowStore]) | |
return { | |
setPanelWidth, | |
handleLayout, | |
getTreeLeafNodes, | |
getBeforeNodesInSameBranch, | |
getAfterNodesInSameBranch, | |
handleOutVarRenameChange, | |
isVarUsedInNodes, | |
removeUsedVarInNodes, | |
isNodeVarsUsedInNodes, | |
isValidConnection, | |
formatTimeFromNow, | |
getNode, | |
getBeforeNodeById, | |
enableShortcuts, | |
disableShortcuts, | |
} | |
} | |
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 || [], | |
}) | |
} | |
}, [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) | |
workflowStore.setState({ appId: appDetail.id }) | |
const handleGetInitialWorkflowData = useCallback(async () => { | |
try { | |
const res = await fetchWorkflowDraft(`/apps/${appDetail.id}/workflows/draft`) | |
setData(res) | |
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: {}, | |
}, | |
}).then((res) => { | |
workflowStore.getState().setDraftUpdatedAt(res.updated_at) | |
handleGetInitialWorkflowData() | |
}) | |
} | |
}) | |
} | |
} | |
}, [appDetail, nodesTemplate, edgesTemplate, workflowStore, setSyncWorkflowDraftHash]) | |
useEffect(() => { | |
handleGetInitialWorkflowData() | |
}, []) | |
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') | |
}, [handleFetchPreloadData, handleFetchAllTools]) | |
useEffect(() => { | |
if (data) | |
workflowStore.getState().setDraftUpdatedAt(data.updated_at) | |
}, [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 toolIcon = useMemo(() => { | |
if (data.type === BlockEnum.Tool) { | |
if (data.provider_type === 'builtin') | |
return buildInTools.find(toolWithProvider => toolWithProvider.id === data.provider_id)?.icon | |
return customTools.find(toolWithProvider => toolWithProvider.id === data.provider_id)?.icon | |
} | |
}, [data, buildInTools, customTools]) | |
return toolIcon | |
} | |