You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

use-workflow-interactions.ts 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  1. import {
  2. useCallback,
  3. useState,
  4. } from 'react'
  5. import { useTranslation } from 'react-i18next'
  6. import { useReactFlow, useStoreApi } from 'reactflow'
  7. import produce from 'immer'
  8. import { useStore, useWorkflowStore } from '../store'
  9. import {
  10. CUSTOM_NODE, DSL_EXPORT_CHECK,
  11. NODE_LAYOUT_HORIZONTAL_PADDING,
  12. NODE_LAYOUT_VERTICAL_PADDING,
  13. WORKFLOW_DATA_UPDATE,
  14. } from '../constants'
  15. import type { Node, WorkflowDataUpdater } from '../types'
  16. import { BlockEnum, ControlMode } from '../types'
  17. import {
  18. getLayoutByDagre,
  19. getLayoutForChildNodes,
  20. initialEdges,
  21. initialNodes,
  22. } from '../utils'
  23. import {
  24. useNodesReadOnly,
  25. useSelectionInteractions,
  26. useWorkflowReadOnly,
  27. } from '../hooks'
  28. import { useEdgesInteractionsWithoutSync } from './use-edges-interactions-without-sync'
  29. import { useNodesInteractionsWithoutSync } from './use-nodes-interactions-without-sync'
  30. import { useNodesSyncDraft } from './use-nodes-sync-draft'
  31. import { WorkflowHistoryEvent, useWorkflowHistory } from './use-workflow-history'
  32. import { useEventEmitterContextContext } from '@/context/event-emitter'
  33. import { fetchWorkflowDraft } from '@/service/workflow'
  34. import { exportAppConfig } from '@/service/apps'
  35. import { useToastContext } from '@/app/components/base/toast'
  36. import { useStore as useAppStore } from '@/app/components/app/store'
  37. export const useWorkflowInteractions = () => {
  38. const workflowStore = useWorkflowStore()
  39. const { handleNodeCancelRunningStatus } = useNodesInteractionsWithoutSync()
  40. const { handleEdgeCancelRunningStatus } = useEdgesInteractionsWithoutSync()
  41. const handleCancelDebugAndPreviewPanel = useCallback(() => {
  42. workflowStore.setState({
  43. showDebugAndPreviewPanel: false,
  44. workflowRunningData: undefined,
  45. })
  46. handleNodeCancelRunningStatus()
  47. handleEdgeCancelRunningStatus()
  48. }, [workflowStore, handleNodeCancelRunningStatus, handleEdgeCancelRunningStatus])
  49. return {
  50. handleCancelDebugAndPreviewPanel,
  51. }
  52. }
  53. export const useWorkflowMoveMode = () => {
  54. const setControlMode = useStore(s => s.setControlMode)
  55. const {
  56. getNodesReadOnly,
  57. } = useNodesReadOnly()
  58. const { handleSelectionCancel } = useSelectionInteractions()
  59. const handleModePointer = useCallback(() => {
  60. if (getNodesReadOnly())
  61. return
  62. setControlMode(ControlMode.Pointer)
  63. }, [getNodesReadOnly, setControlMode])
  64. const handleModeHand = useCallback(() => {
  65. if (getNodesReadOnly())
  66. return
  67. setControlMode(ControlMode.Hand)
  68. handleSelectionCancel()
  69. }, [getNodesReadOnly, setControlMode, handleSelectionCancel])
  70. return {
  71. handleModePointer,
  72. handleModeHand,
  73. }
  74. }
  75. export const useWorkflowOrganize = () => {
  76. const workflowStore = useWorkflowStore()
  77. const store = useStoreApi()
  78. const reactflow = useReactFlow()
  79. const { getNodesReadOnly } = useNodesReadOnly()
  80. const { saveStateToHistory } = useWorkflowHistory()
  81. const { handleSyncWorkflowDraft } = useNodesSyncDraft()
  82. const handleLayout = useCallback(async () => {
  83. if (getNodesReadOnly())
  84. return
  85. workflowStore.setState({ nodeAnimation: true })
  86. const {
  87. getNodes,
  88. edges,
  89. setNodes,
  90. } = store.getState()
  91. const { setViewport } = reactflow
  92. const nodes = getNodes()
  93. const loopAndIterationNodes = nodes.filter(
  94. node => (node.data.type === BlockEnum.Loop || node.data.type === BlockEnum.Iteration)
  95. && !node.parentId
  96. && node.type === CUSTOM_NODE,
  97. )
  98. const childLayoutsMap: Record<string, any> = {}
  99. loopAndIterationNodes.forEach((node) => {
  100. childLayoutsMap[node.id] = getLayoutForChildNodes(node.id, nodes, edges)
  101. })
  102. const containerSizeChanges: Record<string, { width: number, height: number }> = {}
  103. loopAndIterationNodes.forEach((parentNode) => {
  104. const childLayout = childLayoutsMap[parentNode.id]
  105. if (!childLayout) return
  106. let minX = Infinity
  107. let minY = Infinity
  108. let maxX = -Infinity
  109. let maxY = -Infinity
  110. let hasChildren = false
  111. const childNodes = nodes.filter(node => node.parentId === parentNode.id)
  112. childNodes.forEach((node) => {
  113. if (childLayout.node(node.id)) {
  114. hasChildren = true
  115. const childNodeWithPosition = childLayout.node(node.id)
  116. const nodeX = childNodeWithPosition.x - node.width! / 2
  117. const nodeY = childNodeWithPosition.y - node.height! / 2
  118. minX = Math.min(minX, nodeX)
  119. minY = Math.min(minY, nodeY)
  120. maxX = Math.max(maxX, nodeX + node.width!)
  121. maxY = Math.max(maxY, nodeY + node.height!)
  122. }
  123. })
  124. if (hasChildren) {
  125. const requiredWidth = maxX - minX + NODE_LAYOUT_HORIZONTAL_PADDING * 2
  126. const requiredHeight = maxY - minY + NODE_LAYOUT_VERTICAL_PADDING * 2
  127. containerSizeChanges[parentNode.id] = {
  128. width: Math.max(parentNode.width || 0, requiredWidth),
  129. height: Math.max(parentNode.height || 0, requiredHeight),
  130. }
  131. }
  132. })
  133. const nodesWithUpdatedSizes = produce(nodes, (draft) => {
  134. draft.forEach((node) => {
  135. if ((node.data.type === BlockEnum.Loop || node.data.type === BlockEnum.Iteration)
  136. && containerSizeChanges[node.id]) {
  137. node.width = containerSizeChanges[node.id].width
  138. node.height = containerSizeChanges[node.id].height
  139. if (node.data.type === BlockEnum.Loop) {
  140. node.data.width = containerSizeChanges[node.id].width
  141. node.data.height = containerSizeChanges[node.id].height
  142. }
  143. else if (node.data.type === BlockEnum.Iteration) {
  144. node.data.width = containerSizeChanges[node.id].width
  145. node.data.height = containerSizeChanges[node.id].height
  146. }
  147. }
  148. })
  149. })
  150. const layout = getLayoutByDagre(nodesWithUpdatedSizes, edges)
  151. const rankMap = {} as Record<string, Node>
  152. nodesWithUpdatedSizes.forEach((node) => {
  153. if (!node.parentId && node.type === CUSTOM_NODE) {
  154. const rank = layout.node(node.id).rank!
  155. if (!rankMap[rank]) {
  156. rankMap[rank] = node
  157. }
  158. else {
  159. if (rankMap[rank].position.y > node.position.y)
  160. rankMap[rank] = node
  161. }
  162. }
  163. })
  164. const newNodes = produce(nodesWithUpdatedSizes, (draft) => {
  165. draft.forEach((node) => {
  166. if (!node.parentId && node.type === CUSTOM_NODE) {
  167. const nodeWithPosition = layout.node(node.id)
  168. node.position = {
  169. x: nodeWithPosition.x - node.width! / 2,
  170. y: nodeWithPosition.y - node.height! / 2 + rankMap[nodeWithPosition.rank!].height! / 2,
  171. }
  172. }
  173. })
  174. loopAndIterationNodes.forEach((parentNode) => {
  175. const childLayout = childLayoutsMap[parentNode.id]
  176. if (!childLayout) return
  177. const childNodes = draft.filter(node => node.parentId === parentNode.id)
  178. let minX = Infinity
  179. let minY = Infinity
  180. childNodes.forEach((node) => {
  181. if (childLayout.node(node.id)) {
  182. const childNodeWithPosition = childLayout.node(node.id)
  183. const nodeX = childNodeWithPosition.x - node.width! / 2
  184. const nodeY = childNodeWithPosition.y - node.height! / 2
  185. minX = Math.min(minX, nodeX)
  186. minY = Math.min(minY, nodeY)
  187. }
  188. })
  189. childNodes.forEach((node) => {
  190. if (childLayout.node(node.id)) {
  191. const childNodeWithPosition = childLayout.node(node.id)
  192. node.position = {
  193. x: NODE_LAYOUT_HORIZONTAL_PADDING + (childNodeWithPosition.x - node.width! / 2 - minX),
  194. y: NODE_LAYOUT_VERTICAL_PADDING + (childNodeWithPosition.y - node.height! / 2 - minY),
  195. }
  196. }
  197. })
  198. })
  199. })
  200. setNodes(newNodes)
  201. const zoom = 0.7
  202. setViewport({
  203. x: 0,
  204. y: 0,
  205. zoom,
  206. })
  207. saveStateToHistory(WorkflowHistoryEvent.LayoutOrganize)
  208. setTimeout(() => {
  209. handleSyncWorkflowDraft()
  210. })
  211. }, [getNodesReadOnly, store, reactflow, workflowStore, handleSyncWorkflowDraft, saveStateToHistory])
  212. return {
  213. handleLayout,
  214. }
  215. }
  216. export const useWorkflowZoom = () => {
  217. const { handleSyncWorkflowDraft } = useNodesSyncDraft()
  218. const { getWorkflowReadOnly } = useWorkflowReadOnly()
  219. const {
  220. zoomIn,
  221. zoomOut,
  222. zoomTo,
  223. fitView,
  224. } = useReactFlow()
  225. const handleFitView = useCallback(() => {
  226. if (getWorkflowReadOnly())
  227. return
  228. fitView()
  229. handleSyncWorkflowDraft()
  230. }, [getWorkflowReadOnly, fitView, handleSyncWorkflowDraft])
  231. const handleBackToOriginalSize = useCallback(() => {
  232. if (getWorkflowReadOnly())
  233. return
  234. zoomTo(1)
  235. handleSyncWorkflowDraft()
  236. }, [getWorkflowReadOnly, zoomTo, handleSyncWorkflowDraft])
  237. const handleSizeToHalf = useCallback(() => {
  238. if (getWorkflowReadOnly())
  239. return
  240. zoomTo(0.5)
  241. handleSyncWorkflowDraft()
  242. }, [getWorkflowReadOnly, zoomTo, handleSyncWorkflowDraft])
  243. const handleZoomOut = useCallback(() => {
  244. if (getWorkflowReadOnly())
  245. return
  246. zoomOut()
  247. handleSyncWorkflowDraft()
  248. }, [getWorkflowReadOnly, zoomOut, handleSyncWorkflowDraft])
  249. const handleZoomIn = useCallback(() => {
  250. if (getWorkflowReadOnly())
  251. return
  252. zoomIn()
  253. handleSyncWorkflowDraft()
  254. }, [getWorkflowReadOnly, zoomIn, handleSyncWorkflowDraft])
  255. return {
  256. handleFitView,
  257. handleBackToOriginalSize,
  258. handleSizeToHalf,
  259. handleZoomOut,
  260. handleZoomIn,
  261. }
  262. }
  263. export const useWorkflowUpdate = () => {
  264. const reactflow = useReactFlow()
  265. const workflowStore = useWorkflowStore()
  266. const { eventEmitter } = useEventEmitterContextContext()
  267. const handleUpdateWorkflowCanvas = useCallback((payload: WorkflowDataUpdater) => {
  268. const {
  269. nodes,
  270. edges,
  271. viewport,
  272. } = payload
  273. const { setViewport } = reactflow
  274. eventEmitter?.emit({
  275. type: WORKFLOW_DATA_UPDATE,
  276. payload: {
  277. nodes: initialNodes(nodes, edges),
  278. edges: initialEdges(edges, nodes),
  279. },
  280. } as any)
  281. setViewport(viewport)
  282. }, [eventEmitter, reactflow])
  283. const handleRefreshWorkflowDraft = useCallback(() => {
  284. const {
  285. appId,
  286. setSyncWorkflowDraftHash,
  287. setIsSyncingWorkflowDraft,
  288. setEnvironmentVariables,
  289. setEnvSecrets,
  290. setConversationVariables,
  291. } = workflowStore.getState()
  292. setIsSyncingWorkflowDraft(true)
  293. fetchWorkflowDraft(`/apps/${appId}/workflows/draft`).then((response) => {
  294. handleUpdateWorkflowCanvas(response.graph as WorkflowDataUpdater)
  295. setSyncWorkflowDraftHash(response.hash)
  296. setEnvSecrets((response.environment_variables || []).filter(env => env.value_type === 'secret').reduce((acc, env) => {
  297. acc[env.id] = env.value
  298. return acc
  299. }, {} as Record<string, string>))
  300. setEnvironmentVariables(response.environment_variables?.map(env => env.value_type === 'secret' ? { ...env, value: '[__HIDDEN__]' } : env) || [])
  301. // #TODO chatVar sync#
  302. setConversationVariables(response.conversation_variables || [])
  303. }).finally(() => setIsSyncingWorkflowDraft(false))
  304. }, [handleUpdateWorkflowCanvas, workflowStore])
  305. return {
  306. handleUpdateWorkflowCanvas,
  307. handleRefreshWorkflowDraft,
  308. }
  309. }
  310. export const useDSL = () => {
  311. const { t } = useTranslation()
  312. const { notify } = useToastContext()
  313. const { eventEmitter } = useEventEmitterContextContext()
  314. const [exporting, setExporting] = useState(false)
  315. const { doSyncWorkflowDraft } = useNodesSyncDraft()
  316. const appDetail = useAppStore(s => s.appDetail)
  317. const handleExportDSL = useCallback(async (include = false) => {
  318. if (!appDetail)
  319. return
  320. if (exporting)
  321. return
  322. try {
  323. setExporting(true)
  324. await doSyncWorkflowDraft()
  325. const { data } = await exportAppConfig({
  326. appID: appDetail.id,
  327. include,
  328. })
  329. const a = document.createElement('a')
  330. const file = new Blob([data], { type: 'application/yaml' })
  331. a.href = URL.createObjectURL(file)
  332. a.download = `${appDetail.name}.yml`
  333. a.click()
  334. }
  335. catch {
  336. notify({ type: 'error', message: t('app.exportFailed') })
  337. }
  338. finally {
  339. setExporting(false)
  340. }
  341. }, [appDetail, notify, t, doSyncWorkflowDraft, exporting])
  342. const exportCheck = useCallback(async () => {
  343. if (!appDetail)
  344. return
  345. try {
  346. const workflowDraft = await fetchWorkflowDraft(`/apps/${appDetail?.id}/workflows/draft`)
  347. const list = (workflowDraft.environment_variables || []).filter(env => env.value_type === 'secret')
  348. if (list.length === 0) {
  349. handleExportDSL()
  350. return
  351. }
  352. eventEmitter?.emit({
  353. type: DSL_EXPORT_CHECK,
  354. payload: {
  355. data: list,
  356. },
  357. } as any)
  358. }
  359. catch {
  360. notify({ type: 'error', message: t('app.exportFailed') })
  361. }
  362. }, [appDetail, eventEmitter, handleExportDSL, notify, t])
  363. return {
  364. exportCheck,
  365. handleExportDSL,
  366. }
  367. }