use-workflow.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557
  1. import {
  2. useCallback,
  3. useEffect,
  4. useMemo,
  5. useState,
  6. } from 'react'
  7. import dayjs from 'dayjs'
  8. import { uniqBy } from 'lodash-es'
  9. import { useContext } from 'use-context-selector'
  10. import {
  11. getIncomers,
  12. getOutgoers,
  13. useStoreApi,
  14. } from 'reactflow'
  15. import type {
  16. Connection,
  17. } from 'reactflow'
  18. import type {
  19. Edge,
  20. Node,
  21. ValueSelector,
  22. } from '../types'
  23. import {
  24. BlockEnum,
  25. WorkflowRunningStatus,
  26. } from '../types'
  27. import {
  28. useStore,
  29. useWorkflowStore,
  30. } from '../store'
  31. import {
  32. SUPPORT_OUTPUT_VARS_NODE,
  33. } from '../constants'
  34. import { CUSTOM_NOTE_NODE } from '../note-node/constants'
  35. import { findUsedVarNodes, getNodeOutputVars, updateNodeVars } from '../nodes/_base/components/variable/utils'
  36. import { useNodesExtraData } from './use-nodes-data'
  37. import { useWorkflowTemplate } from './use-workflow-template'
  38. import { useStore as useAppStore } from '@/app/components/app/store'
  39. import {
  40. fetchNodesDefaultConfigs,
  41. fetchPublishedWorkflow,
  42. fetchWorkflowDraft,
  43. syncWorkflowDraft,
  44. } from '@/service/workflow'
  45. import type { FetchWorkflowDraftResponse } from '@/types/workflow'
  46. import {
  47. fetchAllBuiltInTools,
  48. fetchAllCustomTools,
  49. fetchAllWorkflowTools,
  50. } from '@/service/tools'
  51. import I18n from '@/context/i18n'
  52. import { CollectionType } from '@/app/components/tools/types'
  53. export const useIsChatMode = () => {
  54. const appDetail = useAppStore(s => s.appDetail)
  55. return appDetail?.mode === 'advanced-chat'
  56. }
  57. export const useWorkflow = () => {
  58. const { locale } = useContext(I18n)
  59. const store = useStoreApi()
  60. const workflowStore = useWorkflowStore()
  61. const nodesExtraData = useNodesExtraData()
  62. const setPanelWidth = useCallback((width: number) => {
  63. localStorage.setItem('workflow-node-panel-width', `${width}`)
  64. workflowStore.setState({ panelWidth: width })
  65. }, [workflowStore])
  66. const getTreeLeafNodes = useCallback((nodeId: string) => {
  67. const {
  68. getNodes,
  69. edges,
  70. } = store.getState()
  71. const nodes = getNodes()
  72. let startNode = nodes.find(node => node.data.type === BlockEnum.Start)
  73. const currentNode = nodes.find(node => node.id === nodeId)
  74. if (currentNode?.parentId)
  75. startNode = nodes.find(node => node.parentId === currentNode.parentId && node.data.isIterationStart)
  76. if (!startNode)
  77. return []
  78. const list: Node[] = []
  79. const preOrder = (root: Node, callback: (node: Node) => void) => {
  80. if (root.id === nodeId)
  81. return
  82. const outgoers = getOutgoers(root, nodes, edges)
  83. if (outgoers.length) {
  84. outgoers.forEach((outgoer) => {
  85. preOrder(outgoer, callback)
  86. })
  87. }
  88. else {
  89. if (root.id !== nodeId)
  90. callback(root)
  91. }
  92. }
  93. preOrder(startNode, (node) => {
  94. list.push(node)
  95. })
  96. const incomers = getIncomers({ id: nodeId } as Node, nodes, edges)
  97. list.push(...incomers)
  98. return uniqBy(list, 'id').filter((item) => {
  99. return SUPPORT_OUTPUT_VARS_NODE.includes(item.data.type)
  100. })
  101. }, [store])
  102. const getBeforeNodesInSameBranch = useCallback((nodeId: string, newNodes?: Node[], newEdges?: Edge[]) => {
  103. const {
  104. getNodes,
  105. edges,
  106. } = store.getState()
  107. const nodes = newNodes || getNodes()
  108. const currentNode = nodes.find(node => node.id === nodeId)
  109. const list: Node[] = []
  110. if (!currentNode)
  111. return list
  112. if (currentNode.parentId) {
  113. const parentNode = nodes.find(node => node.id === currentNode.parentId)
  114. if (parentNode) {
  115. const parentList = getBeforeNodesInSameBranch(parentNode.id)
  116. list.push(...parentList)
  117. }
  118. }
  119. const traverse = (root: Node, callback: (node: Node) => void) => {
  120. if (root) {
  121. const incomers = getIncomers(root, nodes, newEdges || edges)
  122. if (incomers.length) {
  123. incomers.forEach((node) => {
  124. if (!list.find(n => node.id === n.id)) {
  125. callback(node)
  126. traverse(node, callback)
  127. }
  128. })
  129. }
  130. }
  131. }
  132. traverse(currentNode, (node) => {
  133. list.push(node)
  134. })
  135. const length = list.length
  136. if (length) {
  137. return uniqBy(list, 'id').reverse().filter((item) => {
  138. return SUPPORT_OUTPUT_VARS_NODE.includes(item.data.type)
  139. })
  140. }
  141. return []
  142. }, [store])
  143. const getBeforeNodesInSameBranchIncludeParent = useCallback((nodeId: string, newNodes?: Node[], newEdges?: Edge[]) => {
  144. const nodes = getBeforeNodesInSameBranch(nodeId, newNodes, newEdges)
  145. const {
  146. getNodes,
  147. } = store.getState()
  148. const allNodes = getNodes()
  149. const node = allNodes.find(n => n.id === nodeId)
  150. const parentNodeId = node?.parentId
  151. const parentNode = allNodes.find(n => n.id === parentNodeId)
  152. if (parentNode)
  153. nodes.push(parentNode)
  154. return nodes
  155. }, [getBeforeNodesInSameBranch, store])
  156. const getAfterNodesInSameBranch = useCallback((nodeId: string) => {
  157. const {
  158. getNodes,
  159. edges,
  160. } = store.getState()
  161. const nodes = getNodes()
  162. const currentNode = nodes.find(node => node.id === nodeId)!
  163. if (!currentNode)
  164. return []
  165. const list: Node[] = [currentNode]
  166. const traverse = (root: Node, callback: (node: Node) => void) => {
  167. if (root) {
  168. const outgoers = getOutgoers(root, nodes, edges)
  169. if (outgoers.length) {
  170. outgoers.forEach((node) => {
  171. callback(node)
  172. traverse(node, callback)
  173. })
  174. }
  175. }
  176. }
  177. traverse(currentNode, (node) => {
  178. list.push(node)
  179. })
  180. return uniqBy(list, 'id')
  181. }, [store])
  182. const getBeforeNodeById = useCallback((nodeId: string) => {
  183. const {
  184. getNodes,
  185. edges,
  186. } = store.getState()
  187. const nodes = getNodes()
  188. const node = nodes.find(node => node.id === nodeId)!
  189. return getIncomers(node, nodes, edges)
  190. }, [store])
  191. const getIterationNodeChildren = useCallback((nodeId: string) => {
  192. const {
  193. getNodes,
  194. } = store.getState()
  195. const nodes = getNodes()
  196. return nodes.filter(node => node.parentId === nodeId)
  197. }, [store])
  198. const handleOutVarRenameChange = useCallback((nodeId: string, oldValeSelector: ValueSelector, newVarSelector: ValueSelector) => {
  199. const { getNodes, setNodes } = store.getState()
  200. const afterNodes = getAfterNodesInSameBranch(nodeId)
  201. const effectNodes = findUsedVarNodes(oldValeSelector, afterNodes)
  202. if (effectNodes.length > 0) {
  203. const newNodes = getNodes().map((node) => {
  204. if (effectNodes.find(n => n.id === node.id))
  205. return updateNodeVars(node, oldValeSelector, newVarSelector)
  206. return node
  207. })
  208. setNodes(newNodes)
  209. }
  210. // eslint-disable-next-line react-hooks/exhaustive-deps
  211. }, [store])
  212. const isVarUsedInNodes = useCallback((varSelector: ValueSelector) => {
  213. const nodeId = varSelector[0]
  214. const afterNodes = getAfterNodesInSameBranch(nodeId)
  215. const effectNodes = findUsedVarNodes(varSelector, afterNodes)
  216. return effectNodes.length > 0
  217. }, [getAfterNodesInSameBranch])
  218. const removeUsedVarInNodes = useCallback((varSelector: ValueSelector) => {
  219. const nodeId = varSelector[0]
  220. const { getNodes, setNodes } = store.getState()
  221. const afterNodes = getAfterNodesInSameBranch(nodeId)
  222. const effectNodes = findUsedVarNodes(varSelector, afterNodes)
  223. if (effectNodes.length > 0) {
  224. const newNodes = getNodes().map((node) => {
  225. if (effectNodes.find(n => n.id === node.id))
  226. return updateNodeVars(node, varSelector, [])
  227. return node
  228. })
  229. setNodes(newNodes)
  230. }
  231. }, [getAfterNodesInSameBranch, store])
  232. const isNodeVarsUsedInNodes = useCallback((node: Node, isChatMode: boolean) => {
  233. const outputVars = getNodeOutputVars(node, isChatMode)
  234. const isUsed = outputVars.some((varSelector) => {
  235. return isVarUsedInNodes(varSelector)
  236. })
  237. return isUsed
  238. }, [isVarUsedInNodes])
  239. const isValidConnection = useCallback(({ source, target }: Connection) => {
  240. const {
  241. edges,
  242. getNodes,
  243. } = store.getState()
  244. const nodes = getNodes()
  245. const sourceNode: Node = nodes.find(node => node.id === source)!
  246. const targetNode: Node = nodes.find(node => node.id === target)!
  247. if (targetNode.data.isIterationStart)
  248. return false
  249. if (sourceNode.type === CUSTOM_NOTE_NODE || targetNode.type === CUSTOM_NOTE_NODE)
  250. return false
  251. if (sourceNode && targetNode) {
  252. const sourceNodeAvailableNextNodes = nodesExtraData[sourceNode.data.type].availableNextNodes
  253. const targetNodeAvailablePrevNodes = [...nodesExtraData[targetNode.data.type].availablePrevNodes, BlockEnum.Start]
  254. if (!sourceNodeAvailableNextNodes.includes(targetNode.data.type))
  255. return false
  256. if (!targetNodeAvailablePrevNodes.includes(sourceNode.data.type))
  257. return false
  258. }
  259. const hasCycle = (node: Node, visited = new Set()) => {
  260. if (visited.has(node.id))
  261. return false
  262. visited.add(node.id)
  263. for (const outgoer of getOutgoers(node, nodes, edges)) {
  264. if (outgoer.id === source)
  265. return true
  266. if (hasCycle(outgoer, visited))
  267. return true
  268. }
  269. }
  270. return !hasCycle(targetNode)
  271. }, [store, nodesExtraData])
  272. const formatTimeFromNow = useCallback((time: number) => {
  273. return dayjs(time).locale(locale === 'zh-Hans' ? 'zh-cn' : locale).fromNow()
  274. }, [locale])
  275. const getNode = useCallback((nodeId?: string) => {
  276. const { getNodes } = store.getState()
  277. const nodes = getNodes()
  278. return nodes.find(node => node.id === nodeId) || nodes.find(node => node.data.type === BlockEnum.Start)
  279. }, [store])
  280. return {
  281. setPanelWidth,
  282. getTreeLeafNodes,
  283. getBeforeNodesInSameBranch,
  284. getBeforeNodesInSameBranchIncludeParent,
  285. getAfterNodesInSameBranch,
  286. handleOutVarRenameChange,
  287. isVarUsedInNodes,
  288. removeUsedVarInNodes,
  289. isNodeVarsUsedInNodes,
  290. isValidConnection,
  291. formatTimeFromNow,
  292. getNode,
  293. getBeforeNodeById,
  294. getIterationNodeChildren,
  295. }
  296. }
  297. export const useFetchToolsData = () => {
  298. const workflowStore = useWorkflowStore()
  299. const handleFetchAllTools = useCallback(async (type: string) => {
  300. if (type === 'builtin') {
  301. const buildInTools = await fetchAllBuiltInTools()
  302. workflowStore.setState({
  303. buildInTools: buildInTools || [],
  304. })
  305. }
  306. if (type === 'custom') {
  307. const customTools = await fetchAllCustomTools()
  308. workflowStore.setState({
  309. customTools: customTools || [],
  310. })
  311. }
  312. if (type === 'workflow') {
  313. const workflowTools = await fetchAllWorkflowTools()
  314. workflowStore.setState({
  315. workflowTools: workflowTools || [],
  316. })
  317. }
  318. }, [workflowStore])
  319. return {
  320. handleFetchAllTools,
  321. }
  322. }
  323. export const useWorkflowInit = () => {
  324. const workflowStore = useWorkflowStore()
  325. const {
  326. nodes: nodesTemplate,
  327. edges: edgesTemplate,
  328. } = useWorkflowTemplate()
  329. const { handleFetchAllTools } = useFetchToolsData()
  330. const appDetail = useAppStore(state => state.appDetail)!
  331. const setSyncWorkflowDraftHash = useStore(s => s.setSyncWorkflowDraftHash)
  332. const [data, setData] = useState<FetchWorkflowDraftResponse>()
  333. const [isLoading, setIsLoading] = useState(true)
  334. workflowStore.setState({ appId: appDetail.id })
  335. const handleGetInitialWorkflowData = useCallback(async () => {
  336. try {
  337. const res = await fetchWorkflowDraft(`/apps/${appDetail.id}/workflows/draft`)
  338. setData(res)
  339. workflowStore.setState({
  340. envSecrets: (res.environment_variables || []).filter(env => env.value_type === 'secret').reduce((acc, env) => {
  341. acc[env.id] = env.value
  342. return acc
  343. }, {} as Record<string, string>),
  344. environmentVariables: res.environment_variables?.map(env => env.value_type === 'secret' ? { ...env, value: '[__HIDDEN__]' } : env) || [],
  345. // #TODO chatVar sync#
  346. conversationVariables: res.conversation_variables || [],
  347. })
  348. setSyncWorkflowDraftHash(res.hash)
  349. setIsLoading(false)
  350. }
  351. catch (error: any) {
  352. if (error && error.json && !error.bodyUsed && appDetail) {
  353. error.json().then((err: any) => {
  354. if (err.code === 'draft_workflow_not_exist') {
  355. workflowStore.setState({ notInitialWorkflow: true })
  356. syncWorkflowDraft({
  357. url: `/apps/${appDetail.id}/workflows/draft`,
  358. params: {
  359. graph: {
  360. nodes: nodesTemplate,
  361. edges: edgesTemplate,
  362. },
  363. features: {
  364. retriever_resource: { enabled: true },
  365. },
  366. environment_variables: [],
  367. conversation_variables: [],
  368. },
  369. }).then((res) => {
  370. workflowStore.getState().setDraftUpdatedAt(res.updated_at)
  371. handleGetInitialWorkflowData()
  372. })
  373. }
  374. })
  375. }
  376. }
  377. }, [appDetail, nodesTemplate, edgesTemplate, workflowStore, setSyncWorkflowDraftHash])
  378. useEffect(() => {
  379. handleGetInitialWorkflowData()
  380. }, [])
  381. const handleFetchPreloadData = useCallback(async () => {
  382. try {
  383. const nodesDefaultConfigsData = await fetchNodesDefaultConfigs(`/apps/${appDetail?.id}/workflows/default-workflow-block-configs`)
  384. const publishedWorkflow = await fetchPublishedWorkflow(`/apps/${appDetail?.id}/workflows/publish`)
  385. workflowStore.setState({
  386. nodesDefaultConfigs: nodesDefaultConfigsData.reduce((acc, block) => {
  387. if (!acc[block.type])
  388. acc[block.type] = { ...block.config }
  389. return acc
  390. }, {} as Record<string, any>),
  391. })
  392. workflowStore.getState().setPublishedAt(publishedWorkflow?.created_at)
  393. }
  394. catch (e) {
  395. }
  396. }, [workflowStore, appDetail])
  397. useEffect(() => {
  398. handleFetchPreloadData()
  399. handleFetchAllTools('builtin')
  400. handleFetchAllTools('custom')
  401. handleFetchAllTools('workflow')
  402. }, [handleFetchPreloadData, handleFetchAllTools])
  403. useEffect(() => {
  404. if (data) {
  405. workflowStore.getState().setDraftUpdatedAt(data.updated_at)
  406. workflowStore.getState().setToolPublished(data.tool_published)
  407. }
  408. }, [data, workflowStore])
  409. return {
  410. data,
  411. isLoading,
  412. }
  413. }
  414. export const useWorkflowReadOnly = () => {
  415. const workflowStore = useWorkflowStore()
  416. const workflowRunningData = useStore(s => s.workflowRunningData)
  417. const getWorkflowReadOnly = useCallback(() => {
  418. return workflowStore.getState().workflowRunningData?.result.status === WorkflowRunningStatus.Running
  419. }, [workflowStore])
  420. return {
  421. workflowReadOnly: workflowRunningData?.result.status === WorkflowRunningStatus.Running,
  422. getWorkflowReadOnly,
  423. }
  424. }
  425. export const useNodesReadOnly = () => {
  426. const workflowStore = useWorkflowStore()
  427. const workflowRunningData = useStore(s => s.workflowRunningData)
  428. const historyWorkflowData = useStore(s => s.historyWorkflowData)
  429. const isRestoring = useStore(s => s.isRestoring)
  430. const getNodesReadOnly = useCallback(() => {
  431. const {
  432. workflowRunningData,
  433. historyWorkflowData,
  434. isRestoring,
  435. } = workflowStore.getState()
  436. return workflowRunningData?.result.status === WorkflowRunningStatus.Running || historyWorkflowData || isRestoring
  437. }, [workflowStore])
  438. return {
  439. nodesReadOnly: !!(workflowRunningData?.result.status === WorkflowRunningStatus.Running || historyWorkflowData || isRestoring),
  440. getNodesReadOnly,
  441. }
  442. }
  443. export const useToolIcon = (data: Node['data']) => {
  444. const buildInTools = useStore(s => s.buildInTools)
  445. const customTools = useStore(s => s.customTools)
  446. const workflowTools = useStore(s => s.workflowTools)
  447. const toolIcon = useMemo(() => {
  448. if (data.type === BlockEnum.Tool) {
  449. let targetTools = buildInTools
  450. if (data.provider_type === CollectionType.builtIn)
  451. targetTools = buildInTools
  452. else if (data.provider_type === CollectionType.custom)
  453. targetTools = customTools
  454. else
  455. targetTools = workflowTools
  456. return targetTools.find(toolWithProvider => toolWithProvider.id === data.provider_id)?.icon
  457. }
  458. }, [data, buildInTools, customTools, workflowTools])
  459. return toolIcon
  460. }
  461. export const useIsNodeInIteration = (iterationId: string) => {
  462. const store = useStoreApi()
  463. const isNodeInIteration = useCallback((nodeId: string) => {
  464. const {
  465. getNodes,
  466. } = store.getState()
  467. const nodes = getNodes()
  468. const node = nodes.find(node => node.id === nodeId)
  469. if (!node)
  470. return false
  471. if (node.parentId === iterationId)
  472. return true
  473. return false
  474. }, [iterationId, store])
  475. return {
  476. isNodeInIteration,
  477. }
  478. }