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