| 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, |
| } |
| } |
|
|