|
import { |
|
useCallback, |
|
useState, |
|
} from 'react' |
|
import { useTranslation } from 'react-i18next' |
|
import { useReactFlow, useStoreApi } from 'reactflow' |
|
import produce from 'immer' |
|
import { useStore, useWorkflowStore } from '../store' |
|
import { |
|
CUSTOM_NODE, DSL_EXPORT_CHECK, |
|
WORKFLOW_DATA_UPDATE, |
|
} from '../constants' |
|
import type { Node, WorkflowDataUpdater } from '../types' |
|
import { ControlMode } from '../types' |
|
import { |
|
getLayoutByDagre, |
|
initialEdges, |
|
initialNodes, |
|
} from '../utils' |
|
import { |
|
useNodesReadOnly, |
|
useSelectionInteractions, |
|
useWorkflowReadOnly, |
|
} from '../hooks' |
|
import { useEdgesInteractions } from './use-edges-interactions' |
|
import { useNodesInteractions } from './use-nodes-interactions' |
|
import { useNodesSyncDraft } from './use-nodes-sync-draft' |
|
import { WorkflowHistoryEvent, useWorkflowHistory } from './use-workflow-history' |
|
import { useEventEmitterContextContext } from '@/context/event-emitter' |
|
import { fetchWorkflowDraft } from '@/service/workflow' |
|
import { exportAppConfig } from '@/service/apps' |
|
import { useToastContext } from '@/app/components/base/toast' |
|
import { useStore as useAppStore } from '@/app/components/app/store' |
|
|
|
export const useWorkflowInteractions = () => { |
|
const workflowStore = useWorkflowStore() |
|
const { handleNodeCancelRunningStatus } = useNodesInteractions() |
|
const { handleEdgeCancelRunningStatus } = useEdgesInteractions() |
|
|
|
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 layout = getLayoutByDagre(nodes, edges) |
|
const rankMap = {} as Record<string, Node> |
|
|
|
nodes.forEach((node) => { |
|
if (!node.parentId && node.type === CUSTOM_NODE) { |
|
const rank = layout.node(node.id).rank! |
|
|
|
if (!rankMap[rank]) { |
|
rankMap[rank] = node |
|
} |
|
else { |
|
if (rankMap[rank].position.y > node.position.y) |
|
rankMap[rank] = node |
|
} |
|
} |
|
}) |
|
|
|
const newNodes = produce(nodes, (draft) => { |
|
draft.forEach((node) => { |
|
if (!node.parentId && node.type === CUSTOM_NODE) { |
|
const nodeWithPosition = layout.node(node.id) |
|
|
|
node.position = { |
|
x: nodeWithPosition.x - node.width! / 2, |
|
y: nodeWithPosition.y - node.height! / 2 + rankMap[nodeWithPosition.rank!].height! / 2, |
|
} |
|
} |
|
}) |
|
}) |
|
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 workflowStore = useWorkflowStore() |
|
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) |
|
setViewport(viewport) |
|
}, [eventEmitter, reactflow]) |
|
|
|
const handleRefreshWorkflowDraft = useCallback(() => { |
|
const { |
|
appId, |
|
setSyncWorkflowDraftHash, |
|
setIsSyncingWorkflowDraft, |
|
setEnvironmentVariables, |
|
setEnvSecrets, |
|
setConversationVariables, |
|
} = workflowStore.getState() |
|
setIsSyncingWorkflowDraft(true) |
|
fetchWorkflowDraft(`/apps/${appId}/workflows/draft`).then((response) => { |
|
handleUpdateWorkflowCanvas(response.graph as WorkflowDataUpdater) |
|
setSyncWorkflowDraftHash(response.hash) |
|
setEnvSecrets((response.environment_variables || []).filter(env => env.value_type === 'secret').reduce((acc, env) => { |
|
acc[env.id] = env.value |
|
return acc |
|
}, {} as Record<string, string>)) |
|
setEnvironmentVariables(response.environment_variables?.map(env => env.value_type === 'secret' ? { ...env, value: '[__HIDDEN__]' } : env) || []) |
|
|
|
setConversationVariables(response.conversation_variables || []) |
|
}).finally(() => setIsSyncingWorkflowDraft(false)) |
|
}, [handleUpdateWorkflowCanvas, workflowStore]) |
|
|
|
return { |
|
handleUpdateWorkflowCanvas, |
|
handleRefreshWorkflowDraft, |
|
} |
|
} |
|
|
|
export const useDSL = () => { |
|
const { t } = useTranslation() |
|
const { notify } = useToastContext() |
|
const { eventEmitter } = useEventEmitterContextContext() |
|
const [exporting, setExporting] = useState(false) |
|
const { doSyncWorkflowDraft } = useNodesSyncDraft() |
|
|
|
const appDetail = useAppStore(s => s.appDetail) |
|
|
|
const handleExportDSL = useCallback(async (include = false) => { |
|
if (!appDetail) |
|
return |
|
|
|
if (exporting) |
|
return |
|
|
|
try { |
|
setExporting(true) |
|
await doSyncWorkflowDraft() |
|
const { data } = await exportAppConfig({ |
|
appID: appDetail.id, |
|
include, |
|
}) |
|
const a = document.createElement('a') |
|
const file = new Blob([data], { type: 'application/yaml' }) |
|
a.href = URL.createObjectURL(file) |
|
a.download = `${appDetail.name}.yml` |
|
a.click() |
|
} |
|
catch (e) { |
|
notify({ type: 'error', message: t('app.exportFailed') }) |
|
} |
|
finally { |
|
setExporting(false) |
|
} |
|
}, [appDetail, notify, t, doSyncWorkflowDraft, exporting]) |
|
|
|
const exportCheck = useCallback(async () => { |
|
if (!appDetail) |
|
return |
|
try { |
|
const workflowDraft = await fetchWorkflowDraft(`/apps/${appDetail?.id}/workflows/draft`) |
|
const list = (workflowDraft.environment_variables || []).filter(env => env.value_type === 'secret') |
|
if (list.length === 0) { |
|
handleExportDSL() |
|
return |
|
} |
|
eventEmitter?.emit({ |
|
type: DSL_EXPORT_CHECK, |
|
payload: { |
|
data: list, |
|
}, |
|
} as any) |
|
} |
|
catch (e) { |
|
notify({ type: 'error', message: t('app.exportFailed') }) |
|
} |
|
}, [appDetail, eventEmitter, handleExportDSL, notify, t]) |
|
|
|
return { |
|
exportCheck, |
|
handleExportDSL, |
|
} |
|
} |
|
|