@foresthubai/workflow-builder 0.4.3 → 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (414) hide show
  1. package/LICENSE +661 -661
  2. package/NOTICE +16 -16
  3. package/README.md +110 -110
  4. package/dist/BuilderLayout.d.ts.map +1 -1
  5. package/dist/BuilderLayout.js +52 -61
  6. package/dist/BuilderLayout.js.map +1 -1
  7. package/dist/Canvas.d.ts.map +1 -1
  8. package/dist/Canvas.js +10 -3
  9. package/dist/Canvas.js.map +1 -1
  10. package/dist/CanvasEditor.d.ts.map +1 -1
  11. package/dist/CanvasEditor.js.map +1 -1
  12. package/dist/CanvasTabsToolbar.d.ts.map +1 -1
  13. package/dist/CanvasTabsToolbar.js +3 -0
  14. package/dist/CanvasTabsToolbar.js.map +1 -1
  15. package/dist/RightConfigPanel.d.ts.map +1 -1
  16. package/dist/RightConfigPanel.js +3 -0
  17. package/dist/RightConfigPanel.js.map +1 -1
  18. package/dist/WorkflowBuilder.d.ts +7 -13
  19. package/dist/WorkflowBuilder.d.ts.map +1 -1
  20. package/dist/WorkflowBuilder.js +25 -13
  21. package/dist/WorkflowBuilder.js.map +1 -1
  22. package/dist/cn.d.ts.map +1 -1
  23. package/dist/cn.js +3 -0
  24. package/dist/cn.js.map +1 -1
  25. package/dist/components/ui/add-button.d.ts.map +1 -1
  26. package/dist/components/ui/add-button.js +3 -0
  27. package/dist/components/ui/add-button.js.map +1 -1
  28. package/dist/components/ui/alert-dialog.d.ts.map +1 -1
  29. package/dist/components/ui/alert-dialog.js +3 -0
  30. package/dist/components/ui/alert-dialog.js.map +1 -1
  31. package/dist/components/ui/alert.d.ts.map +1 -1
  32. package/dist/components/ui/alert.js +3 -0
  33. package/dist/components/ui/alert.js.map +1 -1
  34. package/dist/components/ui/badge.d.ts.map +1 -1
  35. package/dist/components/ui/badge.js.map +1 -1
  36. package/dist/components/ui/button.d.ts.map +1 -1
  37. package/dist/components/ui/button.js +3 -0
  38. package/dist/components/ui/button.js.map +1 -1
  39. package/dist/components/ui/card.d.ts.map +1 -1
  40. package/dist/components/ui/card.js +3 -0
  41. package/dist/components/ui/card.js.map +1 -1
  42. package/dist/components/ui/checkbox.d.ts.map +1 -1
  43. package/dist/components/ui/checkbox.js +3 -0
  44. package/dist/components/ui/checkbox.js.map +1 -1
  45. package/dist/components/ui/collapsible.d.ts.map +1 -1
  46. package/dist/components/ui/collapsible.js +3 -0
  47. package/dist/components/ui/collapsible.js.map +1 -1
  48. package/dist/components/ui/command.d.ts +2 -2
  49. package/dist/components/ui/command.d.ts.map +1 -1
  50. package/dist/components/ui/command.js +3 -0
  51. package/dist/components/ui/command.js.map +1 -1
  52. package/dist/components/ui/delete-button.d.ts.map +1 -1
  53. package/dist/components/ui/delete-button.js +3 -0
  54. package/dist/components/ui/delete-button.js.map +1 -1
  55. package/dist/components/ui/dialog.d.ts.map +1 -1
  56. package/dist/components/ui/dialog.js +3 -0
  57. package/dist/components/ui/dialog.js.map +1 -1
  58. package/dist/components/ui/dropdown-menu.d.ts.map +1 -1
  59. package/dist/components/ui/dropdown-menu.js +3 -0
  60. package/dist/components/ui/dropdown-menu.js.map +1 -1
  61. package/dist/components/ui/input.d.ts +1 -1
  62. package/dist/components/ui/input.d.ts.map +1 -1
  63. package/dist/components/ui/input.js +3 -0
  64. package/dist/components/ui/input.js.map +1 -1
  65. package/dist/components/ui/label.d.ts.map +1 -1
  66. package/dist/components/ui/label.js +3 -0
  67. package/dist/components/ui/label.js.map +1 -1
  68. package/dist/components/ui/readonly-banner.d.ts.map +1 -1
  69. package/dist/components/ui/readonly-banner.js +3 -0
  70. package/dist/components/ui/readonly-banner.js.map +1 -1
  71. package/dist/components/ui/resizable.d.ts +1 -1
  72. package/dist/components/ui/resizable.d.ts.map +1 -1
  73. package/dist/components/ui/resizable.js +3 -0
  74. package/dist/components/ui/resizable.js.map +1 -1
  75. package/dist/components/ui/scroll-area.d.ts.map +1 -1
  76. package/dist/components/ui/scroll-area.js +3 -0
  77. package/dist/components/ui/scroll-area.js.map +1 -1
  78. package/dist/components/ui/select.d.ts.map +1 -1
  79. package/dist/components/ui/select.js +3 -0
  80. package/dist/components/ui/select.js.map +1 -1
  81. package/dist/components/ui/separator.d.ts.map +1 -1
  82. package/dist/components/ui/separator.js +3 -0
  83. package/dist/components/ui/separator.js.map +1 -1
  84. package/dist/components/ui/switch.d.ts.map +1 -1
  85. package/dist/components/ui/switch.js +3 -0
  86. package/dist/components/ui/switch.js.map +1 -1
  87. package/dist/components/ui/textarea.d.ts +1 -1
  88. package/dist/components/ui/textarea.d.ts.map +1 -1
  89. package/dist/components/ui/textarea.js +3 -0
  90. package/dist/components/ui/textarea.js.map +1 -1
  91. package/dist/components/ui/toast.d.ts.map +1 -1
  92. package/dist/components/ui/toast.js +3 -0
  93. package/dist/components/ui/toast.js.map +1 -1
  94. package/dist/components/ui/toaster.d.ts.map +1 -1
  95. package/dist/components/ui/toaster.js +3 -0
  96. package/dist/components/ui/toaster.js.map +1 -1
  97. package/dist/components/ui/toggle-group.d.ts.map +1 -1
  98. package/dist/components/ui/toggle-group.js +3 -0
  99. package/dist/components/ui/toggle-group.js.map +1 -1
  100. package/dist/components/ui/toggle.d.ts.map +1 -1
  101. package/dist/components/ui/toggle.js +3 -0
  102. package/dist/components/ui/toggle.js.map +1 -1
  103. package/dist/components/ui/tooltip.d.ts.map +1 -1
  104. package/dist/components/ui/tooltip.js +3 -0
  105. package/dist/components/ui/tooltip.js.map +1 -1
  106. package/dist/dialogs/NodePickerDialog.d.ts.map +1 -1
  107. package/dist/dialogs/NodePickerDialog.js +3 -0
  108. package/dist/dialogs/NodePickerDialog.js.map +1 -1
  109. package/dist/dialogs/ValidationDialog.d.ts.map +1 -1
  110. package/dist/dialogs/ValidationDialog.js +3 -0
  111. package/dist/dialogs/ValidationDialog.js.map +1 -1
  112. package/dist/graph/BaseNode.d.ts.map +1 -1
  113. package/dist/graph/BaseNode.js +21 -16
  114. package/dist/graph/BaseNode.js.map +1 -1
  115. package/dist/graph/CustomEdge.d.ts.map +1 -1
  116. package/dist/graph/CustomEdge.js +4 -1
  117. package/dist/graph/CustomEdge.js.map +1 -1
  118. package/dist/graph/CustomNode.d.ts.map +1 -1
  119. package/dist/graph/CustomNode.js +3 -0
  120. package/dist/graph/CustomNode.js.map +1 -1
  121. package/dist/graph/FunctionCallNode.d.ts.map +1 -1
  122. package/dist/graph/FunctionCallNode.js.map +1 -1
  123. package/dist/graph/PortHandle.d.ts.map +1 -1
  124. package/dist/graph/PortHandle.js.map +1 -1
  125. package/dist/graph/reactFlowRegistry.d.ts.map +1 -1
  126. package/dist/graph/reactFlowRegistry.js +3 -0
  127. package/dist/graph/reactFlowRegistry.js.map +1 -1
  128. package/dist/hooks/use-toast.d.ts.map +1 -1
  129. package/dist/hooks/use-toast.js +3 -0
  130. package/dist/hooks/use-toast.js.map +1 -1
  131. package/dist/hooks/useAvailableVariables.d.ts.map +1 -1
  132. package/dist/hooks/useAvailableVariables.js +3 -0
  133. package/dist/hooks/useAvailableVariables.js.map +1 -1
  134. package/dist/hooks/useCanvasHistory.d.ts.map +1 -1
  135. package/dist/hooks/useCanvasHistory.js +3 -0
  136. package/dist/hooks/useCanvasHistory.js.map +1 -1
  137. package/dist/hooks/useCanvasTabs.d.ts.map +1 -1
  138. package/dist/hooks/useCanvasTabs.js +3 -0
  139. package/dist/hooks/useCanvasTabs.js.map +1 -1
  140. package/dist/hooks/useFunctionDiagnosticsSync.d.ts.map +1 -1
  141. package/dist/hooks/useFunctionDiagnosticsSync.js +3 -0
  142. package/dist/hooks/useFunctionDiagnosticsSync.js.map +1 -1
  143. package/dist/hooks/useFunctionRegistry.d.ts.map +1 -1
  144. package/dist/hooks/useFunctionRegistry.js +3 -0
  145. package/dist/hooks/useFunctionRegistry.js.map +1 -1
  146. package/dist/hooks/useFunctions.d.ts.map +1 -1
  147. package/dist/hooks/useFunctions.js +3 -0
  148. package/dist/hooks/useFunctions.js.map +1 -1
  149. package/dist/hooks/useGraph.d.ts.map +1 -1
  150. package/dist/hooks/useGraph.js +3 -0
  151. package/dist/hooks/useGraph.js.map +1 -1
  152. package/dist/hooks/useNodeDefinitions.d.ts.map +1 -1
  153. package/dist/hooks/useNodeDefinitions.js +3 -0
  154. package/dist/hooks/useNodeDefinitions.js.map +1 -1
  155. package/dist/hooks/useParamErrors.d.ts.map +1 -1
  156. package/dist/hooks/useParamErrors.js +3 -0
  157. package/dist/hooks/useParamErrors.js.map +1 -1
  158. package/dist/hooks/useResolvedTheme.d.ts.map +1 -1
  159. package/dist/hooks/useResolvedTheme.js +3 -0
  160. package/dist/hooks/useResolvedTheme.js.map +1 -1
  161. package/dist/hooks/useResourceDiagnosticsSync.d.ts.map +1 -1
  162. package/dist/hooks/useResourceDiagnosticsSync.js +3 -0
  163. package/dist/hooks/useResourceDiagnosticsSync.js.map +1 -1
  164. package/dist/hooks/useSuppressThemeTransition.d.ts.map +1 -1
  165. package/dist/hooks/useSuppressThemeTransition.js +3 -0
  166. package/dist/hooks/useSuppressThemeTransition.js.map +1 -1
  167. package/dist/hooks/useWorkflowSerialization.d.ts.map +1 -1
  168. package/dist/hooks/useWorkflowSerialization.js +3 -0
  169. package/dist/hooks/useWorkflowSerialization.js.map +1 -1
  170. package/dist/i18n/index.d.ts.map +1 -1
  171. package/dist/i18n/index.js +3 -0
  172. package/dist/i18n/index.js.map +1 -1
  173. package/dist/index.d.ts +1 -1
  174. package/dist/index.d.ts.map +1 -1
  175. package/dist/index.js +3 -0
  176. package/dist/index.js.map +1 -1
  177. package/dist/inputs/ExpressionInput.d.ts.map +1 -1
  178. package/dist/inputs/ExpressionInput.js +3 -0
  179. package/dist/inputs/ExpressionInput.js.map +1 -1
  180. package/dist/inputs/ParameterEditor.d.ts.map +1 -1
  181. package/dist/inputs/ParameterEditor.js +3 -0
  182. package/dist/inputs/ParameterEditor.js.map +1 -1
  183. package/dist/inputs/PortSection.d.ts.map +1 -1
  184. package/dist/inputs/PortSection.js.map +1 -1
  185. package/dist/mode.d.ts +11 -0
  186. package/dist/mode.d.ts.map +1 -0
  187. package/dist/mode.js +8 -0
  188. package/dist/mode.js.map +1 -0
  189. package/dist/panels/BuilderSidebar.d.ts.map +1 -1
  190. package/dist/panels/BuilderSidebar.js +3 -0
  191. package/dist/panels/BuilderSidebar.js.map +1 -1
  192. package/dist/panels/ChannelConfigPanel.d.ts.map +1 -1
  193. package/dist/panels/ChannelConfigPanel.js +3 -0
  194. package/dist/panels/ChannelConfigPanel.js.map +1 -1
  195. package/dist/panels/ChannelsPanel.d.ts.map +1 -1
  196. package/dist/panels/ChannelsPanel.js +3 -0
  197. package/dist/panels/ChannelsPanel.js.map +1 -1
  198. package/dist/panels/DebugConsolePanel.d.ts.map +1 -1
  199. package/dist/panels/DebugConsolePanel.js +3 -0
  200. package/dist/panels/DebugConsolePanel.js.map +1 -1
  201. package/dist/panels/DebugContextPanel.d.ts.map +1 -1
  202. package/dist/panels/DebugContextPanel.js +3 -0
  203. package/dist/panels/DebugContextPanel.js.map +1 -1
  204. package/dist/panels/DebugExternalIOPanel.d.ts.map +1 -1
  205. package/dist/panels/DebugExternalIOPanel.js +3 -0
  206. package/dist/panels/DebugExternalIOPanel.js.map +1 -1
  207. package/dist/panels/DiagnosticsPanel.d.ts.map +1 -1
  208. package/dist/panels/DiagnosticsPanel.js +3 -0
  209. package/dist/panels/DiagnosticsPanel.js.map +1 -1
  210. package/dist/panels/EdgeConfigPanel.d.ts.map +1 -1
  211. package/dist/panels/EdgeConfigPanel.js +4 -1
  212. package/dist/panels/EdgeConfigPanel.js.map +1 -1
  213. package/dist/panels/FunctionConfigPanel.d.ts.map +1 -1
  214. package/dist/panels/FunctionConfigPanel.js +4 -1
  215. package/dist/panels/FunctionConfigPanel.js.map +1 -1
  216. package/dist/panels/FunctionListPanel.d.ts.map +1 -1
  217. package/dist/panels/FunctionListPanel.js +3 -0
  218. package/dist/panels/FunctionListPanel.js.map +1 -1
  219. package/dist/panels/MemoryConfigPanel.d.ts.map +1 -1
  220. package/dist/panels/MemoryConfigPanel.js +3 -0
  221. package/dist/panels/MemoryConfigPanel.js.map +1 -1
  222. package/dist/panels/MemoryPanel.d.ts.map +1 -1
  223. package/dist/panels/MemoryPanel.js +3 -0
  224. package/dist/panels/MemoryPanel.js.map +1 -1
  225. package/dist/panels/ModelConfigPanel.d.ts.map +1 -1
  226. package/dist/panels/ModelConfigPanel.js +3 -0
  227. package/dist/panels/ModelConfigPanel.js.map +1 -1
  228. package/dist/panels/ModelsPanel.d.ts.map +1 -1
  229. package/dist/panels/ModelsPanel.js +3 -0
  230. package/dist/panels/ModelsPanel.js.map +1 -1
  231. package/dist/panels/NodeConfigPanel.d.ts.map +1 -1
  232. package/dist/panels/NodeConfigPanel.js +4 -1
  233. package/dist/panels/NodeConfigPanel.js.map +1 -1
  234. package/dist/panels/NodeLibrary.d.ts.map +1 -1
  235. package/dist/panels/NodeLibrary.js +4 -1
  236. package/dist/panels/NodeLibrary.js.map +1 -1
  237. package/dist/panels/ResourceConfigPanel.d.ts.map +1 -1
  238. package/dist/panels/ResourceConfigPanel.js +4 -1
  239. package/dist/panels/ResourceConfigPanel.js.map +1 -1
  240. package/dist/panels/ResourceListPanel.d.ts.map +1 -1
  241. package/dist/panels/ResourceListPanel.js +4 -1
  242. package/dist/panels/ResourceListPanel.js.map +1 -1
  243. package/dist/panels/VariableConfigPanel.d.ts.map +1 -1
  244. package/dist/panels/VariableConfigPanel.js +4 -1
  245. package/dist/panels/VariableConfigPanel.js.map +1 -1
  246. package/dist/panels/VariablesPanel.d.ts.map +1 -1
  247. package/dist/panels/VariablesPanel.js +4 -1
  248. package/dist/panels/VariablesPanel.js.map +1 -1
  249. package/dist/stores/canvasStore.d.ts.map +1 -1
  250. package/dist/stores/canvasStore.js +3 -0
  251. package/dist/stores/canvasStore.js.map +1 -1
  252. package/dist/stores/debugStore.d.ts.map +1 -1
  253. package/dist/stores/debugStore.js +3 -0
  254. package/dist/stores/debugStore.js.map +1 -1
  255. package/dist/stores/diagnosticsStore.d.ts.map +1 -1
  256. package/dist/stores/diagnosticsStore.js +3 -0
  257. package/dist/stores/diagnosticsStore.js.map +1 -1
  258. package/dist/stores/editorStore.d.ts +1 -1
  259. package/dist/stores/editorStore.d.ts.map +1 -1
  260. package/dist/stores/editorStore.js +3 -0
  261. package/dist/stores/editorStore.js.map +1 -1
  262. package/dist/utils/categoryConstants.d.ts.map +1 -1
  263. package/dist/utils/categoryConstants.js +3 -0
  264. package/dist/utils/categoryConstants.js.map +1 -1
  265. package/dist/utils/channelOperations.d.ts.map +1 -1
  266. package/dist/utils/channelOperations.js +3 -0
  267. package/dist/utils/channelOperations.js.map +1 -1
  268. package/dist/utils/connectionRules.d.ts +3 -1
  269. package/dist/utils/connectionRules.d.ts.map +1 -1
  270. package/dist/utils/connectionRules.js +11 -16
  271. package/dist/utils/connectionRules.js.map +1 -1
  272. package/dist/utils/functionOperations.d.ts.map +1 -1
  273. package/dist/utils/functionOperations.js +3 -0
  274. package/dist/utils/functionOperations.js.map +1 -1
  275. package/dist/utils/graphOperations.d.ts.map +1 -1
  276. package/dist/utils/graphOperations.js +3 -0
  277. package/dist/utils/graphOperations.js.map +1 -1
  278. package/dist/utils/history.d.ts.map +1 -1
  279. package/dist/utils/history.js +4 -1
  280. package/dist/utils/history.js.map +1 -1
  281. package/dist/utils/memoryOperations.d.ts.map +1 -1
  282. package/dist/utils/memoryOperations.js +3 -0
  283. package/dist/utils/memoryOperations.js.map +1 -1
  284. package/dist/utils/migrateFunctionNodes.d.ts.map +1 -1
  285. package/dist/utils/migrateFunctionNodes.js +3 -0
  286. package/dist/utils/migrateFunctionNodes.js.map +1 -1
  287. package/dist/utils/modelOperations.d.ts.map +1 -1
  288. package/dist/utils/modelOperations.js +3 -0
  289. package/dist/utils/modelOperations.js.map +1 -1
  290. package/dist/utils/paramDisplay.d.ts.map +1 -1
  291. package/dist/utils/paramDisplay.js +3 -0
  292. package/dist/utils/paramDisplay.js.map +1 -1
  293. package/dist/utils/resourceHelpers.d.ts.map +1 -1
  294. package/dist/utils/resourceHelpers.js +3 -0
  295. package/dist/utils/resourceHelpers.js.map +1 -1
  296. package/dist/utils/translation.d.ts.map +1 -1
  297. package/dist/utils/translation.js +3 -0
  298. package/dist/utils/translation.js.map +1 -1
  299. package/dist/utils/variableOperations.d.ts.map +1 -1
  300. package/dist/utils/variableOperations.js +3 -0
  301. package/dist/utils/variableOperations.js.map +1 -1
  302. package/package.json +2 -2
  303. package/src/BuilderLayout.tsx +338 -345
  304. package/src/Canvas.tsx +270 -261
  305. package/src/CanvasEditor.tsx +146 -142
  306. package/src/CanvasTabsToolbar.tsx +180 -176
  307. package/src/RightConfigPanel.tsx +270 -266
  308. package/src/WorkflowBuilder.tsx +423 -408
  309. package/src/cn.ts +10 -6
  310. package/src/components/ui/add-button.tsx +43 -39
  311. package/src/components/ui/alert-dialog.tsx +145 -141
  312. package/src/components/ui/alert.tsx +63 -59
  313. package/src/components/ui/badge.tsx +40 -36
  314. package/src/components/ui/button.tsx +89 -85
  315. package/src/components/ui/card.tsx +83 -79
  316. package/src/components/ui/checkbox.tsx +32 -28
  317. package/src/components/ui/collapsible.tsx +13 -9
  318. package/src/components/ui/command.tsx +157 -153
  319. package/src/components/ui/delete-button.tsx +27 -23
  320. package/src/components/ui/dialog.tsx +129 -125
  321. package/src/components/ui/dropdown-menu.tsx +202 -198
  322. package/src/components/ui/input.tsx +59 -55
  323. package/src/components/ui/label.tsx +28 -24
  324. package/src/components/ui/readonly-banner.tsx +19 -15
  325. package/src/components/ui/resizable.tsx +47 -43
  326. package/src/components/ui/scroll-area.tsx +106 -102
  327. package/src/components/ui/select.tsx +164 -160
  328. package/src/components/ui/separator.tsx +33 -29
  329. package/src/components/ui/switch.tsx +31 -27
  330. package/src/components/ui/textarea.tsx +55 -51
  331. package/src/components/ui/toast.tsx +131 -127
  332. package/src/components/ui/toaster.tsx +37 -33
  333. package/src/components/ui/toggle-group.tsx +63 -59
  334. package/src/components/ui/toggle.tsx +47 -43
  335. package/src/components/ui/tooltip.tsx +36 -32
  336. package/src/dialogs/NodePickerDialog.tsx +88 -84
  337. package/src/dialogs/ValidationDialog.tsx +188 -184
  338. package/src/graph/BaseNode.tsx +563 -557
  339. package/src/graph/CustomEdge.tsx +189 -185
  340. package/src/graph/CustomNode.tsx +20 -16
  341. package/src/graph/FunctionCallNode.tsx +34 -30
  342. package/src/graph/PortHandle.tsx +193 -189
  343. package/src/graph/reactFlowRegistry.ts +30 -26
  344. package/src/hooks/use-toast.ts +129 -125
  345. package/src/hooks/useAvailableVariables.ts +24 -20
  346. package/src/hooks/useCanvasHistory.ts +26 -22
  347. package/src/hooks/useCanvasTabs.ts +172 -168
  348. package/src/hooks/useFunctionDiagnosticsSync.ts +44 -40
  349. package/src/hooks/useFunctionRegistry.ts +30 -26
  350. package/src/hooks/useFunctions.ts +48 -44
  351. package/src/hooks/useGraph.ts +165 -161
  352. package/src/hooks/useNodeDefinitions.ts +86 -82
  353. package/src/hooks/useParamErrors.ts +30 -26
  354. package/src/hooks/useResolvedTheme.ts +34 -30
  355. package/src/hooks/useResourceDiagnosticsSync.ts +62 -58
  356. package/src/hooks/useSuppressThemeTransition.ts +83 -79
  357. package/src/hooks/useWorkflowSerialization.ts +145 -141
  358. package/src/i18n/index.ts +57 -53
  359. package/src/i18n/locales/de.json +501 -501
  360. package/src/i18n/locales/en.json +557 -557
  361. package/src/index.ts +31 -27
  362. package/src/inputs/ExpressionInput.tsx +301 -297
  363. package/src/inputs/ParameterEditor.tsx +519 -515
  364. package/src/inputs/PortSection.tsx +148 -144
  365. package/src/mode.ts +15 -0
  366. package/src/panels/BuilderSidebar.tsx +305 -301
  367. package/src/panels/ChannelConfigPanel.tsx +53 -49
  368. package/src/panels/ChannelsPanel.tsx +32 -28
  369. package/src/panels/DebugConsolePanel.tsx +77 -73
  370. package/src/panels/DebugContextPanel.tsx +81 -77
  371. package/src/panels/DebugExternalIOPanel.tsx +184 -180
  372. package/src/panels/DiagnosticsPanel.tsx +174 -170
  373. package/src/panels/EdgeConfigPanel.tsx +108 -104
  374. package/src/panels/FunctionConfigPanel.tsx +183 -179
  375. package/src/panels/FunctionListPanel.tsx +49 -45
  376. package/src/panels/MemoryConfigPanel.tsx +59 -55
  377. package/src/panels/MemoryPanel.tsx +44 -40
  378. package/src/panels/ModelConfigPanel.tsx +45 -41
  379. package/src/panels/ModelsPanel.tsx +40 -36
  380. package/src/panels/NodeConfigPanel.tsx +634 -630
  381. package/src/panels/NodeLibrary.tsx +292 -288
  382. package/src/panels/ResourceConfigPanel.tsx +136 -132
  383. package/src/panels/ResourceListPanel.tsx +117 -113
  384. package/src/panels/VariableConfigPanel.tsx +165 -161
  385. package/src/panels/VariablesPanel.tsx +149 -145
  386. package/src/stores/canvasStore.test.ts +48 -44
  387. package/src/stores/canvasStore.ts +249 -245
  388. package/src/stores/debugStore.ts +78 -74
  389. package/src/stores/diagnosticsStore.ts +134 -130
  390. package/src/stores/editorStore.ts +194 -190
  391. package/src/styles/index.css +526 -526
  392. package/src/utils/categoryConstants.ts +30 -26
  393. package/src/utils/channelOperations.ts +90 -86
  394. package/src/utils/connectionRules.ts +140 -137
  395. package/src/utils/functionOperations.ts +183 -179
  396. package/src/utils/graphOperations.ts +554 -550
  397. package/src/utils/history.test.ts +92 -0
  398. package/src/utils/history.ts +211 -207
  399. package/src/utils/memoryOperations.ts +61 -57
  400. package/src/utils/migrateFunctionNodes.ts +111 -107
  401. package/src/utils/modelOperations.ts +59 -55
  402. package/src/utils/paramDisplay.ts +75 -71
  403. package/src/utils/resourceHelpers.ts +36 -32
  404. package/src/utils/translation.ts +32 -28
  405. package/src/utils/variableOperations.ts +79 -75
  406. package/tailwind-preset.ts +170 -166
  407. package/dist/lib/utils.d.ts +0 -3
  408. package/dist/lib/utils.d.ts.map +0 -1
  409. package/dist/lib/utils.js +0 -6
  410. package/dist/lib/utils.js.map +0 -1
  411. package/dist/toolbars/CanvasTabsToolbar.d.ts +0 -11
  412. package/dist/toolbars/CanvasTabsToolbar.d.ts.map +0 -1
  413. package/dist/toolbars/CanvasTabsToolbar.js +0 -101
  414. package/dist/toolbars/CanvasTabsToolbar.js.map +0 -1
@@ -1,408 +1,423 @@
1
- import type { ApiWorkflow } from "@foresthubai/workflow-core/workflow";
2
- import type { ModelInfo } from "@foresthubai/workflow-core/model";
3
- import {
4
- validateWorkflowState,
5
- validateChannel,
6
- validateMemory,
7
- validateModel,
8
- type Diagnostic,
9
- type ValidationResult,
10
- } from "@foresthubai/workflow-core/diagnostics";
11
- import { forwardRef, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useRef, useState } from "react";
12
- import { I18nextProvider } from "react-i18next";
13
-
14
- import i18n from "./i18n";
15
- import { toast } from "./hooks/use-toast";
16
- import ValidationDialog from "./dialogs/ValidationDialog";
17
- import { BuilderLayout } from "./BuilderLayout";
18
- import { TooltipProvider } from "./components/ui/tooltip";
19
- import { Toaster } from "./components/ui/toaster";
20
- import { useCanvasTabs } from "./hooks/useCanvasTabs";
21
- import { useResourceDiagnosticsSync } from "./hooks/useResourceDiagnosticsSync";
22
- import { useFunctionDiagnosticsSync } from "./hooks/useFunctionDiagnosticsSync";
23
- import { useSuppressThemeTransition } from "./hooks/useSuppressThemeTransition";
24
- import { useFunctions } from "./hooks/useFunctions";
25
- import { useWorkflowSerialization, readStateFromStores } from "./hooks/useWorkflowSerialization";
26
- import {
27
- getAllCanvasStores,
28
- getOrCreateCanvasStore,
29
- subscribeCanvasRegistryChanges,
30
- MAIN_CANVAS_ID,
31
- type CanvasStore,
32
- } from "./stores/canvasStore";
33
- import { useDebugStore, type DebugSessionPhase } from "./stores/debugStore";
34
- import { useEditorStore } from "./stores/editorStore";
35
-
36
- /** BuilderMode steers the overall behavior of the workflow builder. */
37
- export type BuilderMode = { type: "edit" } | { type: "preview" } | { type: "debug" };
38
-
39
- /** True when canvas mutations should be blocked (preview or debug). */
40
- export function isReadOnly(mode: BuilderMode): boolean {
41
- return mode.type !== "edit";
42
- }
43
-
44
- /** Type guard for preview mode. */
45
- export function isPreview(mode: BuilderMode): boolean {
46
- return mode.type === "preview";
47
- }
48
-
49
- // ============================================================================
50
- // Public contract
51
- // ============================================================================
52
-
53
- export interface WorkflowBuilderProps {
54
- /** Workflow loaded on mount. If none is provided, an empty workflow is created. */
55
- initialWorkflow?: ApiWorkflow;
56
- /** Builder mode on mount. Defaults to { type: "edit" }. */
57
- initialMode?: BuilderMode;
58
- /**
59
- * Static model catalog the models the llmproxy supports. Shown as the
60
- * built-in options in agent model pickers. Self-hosted/custom models are
61
- * declared in the Models tab instead. Defaults to [] (empty dropdown).
62
- */
63
- models?: ModelInfo[];
64
- /**
65
- * UI language (e.g. "en", "de"). The host owns locale; the builder follows.
66
- * Defaults to "en". The builder never auto-detects language.
67
- */
68
- language?: string;
69
-
70
- // ── Embedder-fulfilled actions (builder asks, embedder does) ──
71
- /** A node requested embedder-side testing (e.g. Agent "Test" button). */
72
- onTestNode?: (nodeId: string) => void;
73
- /** Step request from the in-builder debug panel embedder forwards to the engine. */
74
- onDebugStep?: (nodeId?: string) => void;
75
-
76
- // ── Lifecycle events ──
77
- /** Fires after any domain-state mutation. Pull current state via handle.exportWorkflow(). */
78
- onChange?: () => void;
79
- /**
80
- * Undo/redo availability for the ACTIVE canvas changed — on history mutation,
81
- * undo/redo, or a tab switch (each canvas has its own history). For wiring host
82
- * undo/redo buttons.
83
- */
84
- onHistoryChange?: (state: { canUndo: boolean; canRedo: boolean }) => void;
85
- /** Unexpected error during builder operations (e.g. failed load). */
86
- onError?: (error: Error) => void;
87
- }
88
-
89
- export interface WorkflowBuilderHandle {
90
- // State I/O
91
- loadWorkflow: (workflow: ApiWorkflow) => void;
92
- exportWorkflow: () => ApiWorkflow;
93
- clear: () => void;
94
-
95
- // Mode (replaces preview/debug entry-point props)
96
- setMode: (mode: BuilderMode) => void;
97
- getMode: () => BuilderMode;
98
-
99
- // Initiate the in-builder validation process which will either show the validation dialog or a toast if clean.
100
- validate: () => void;
101
-
102
- // History (so embedder chrome can wire undo/redo buttons)
103
- undo: () => void;
104
- redo: () => void;
105
-
106
- // Debug (embedder pushes engine events for visualization)
107
- setDebugPhase: (phase: DebugSessionPhase) => void;
108
- }
109
-
110
- // ============================================================================
111
- // Component — owns tabs/functions/dialogs; exposes the handle; delegates
112
- // layout/canvas rendering to BuilderLayout.
113
- // ============================================================================
114
-
115
- export const WorkflowBuilder = forwardRef<WorkflowBuilderHandle, WorkflowBuilderProps>(
116
- function WorkflowBuilder(props, ref) {
117
- const {
118
- initialWorkflow,
119
- initialMode,
120
- models,
121
- language,
122
- onTestNode,
123
- onDebugStep,
124
- onChange,
125
- onHistoryChange,
126
- onError,
127
- } = props;
128
-
129
- // Host drives locale. useLayoutEffect (not useEffect) so the language is set
130
- // before paint — mounting with language="de" shows German on the first frame
131
- // rather than flashing English. changeLanguage is sync here (bundled resources).
132
- useLayoutEffect(() => {
133
- if (language && i18n.language !== language) i18n.changeLanguage(language);
134
- }, [language]);
135
-
136
- const { importProject, exportProject, clearProject } = useWorkflowSerialization();
137
-
138
- // Color-mode toggles should snap, not fade — see hook docs.
139
- useSuppressThemeTransition();
140
-
141
- // Keep project-scoped (channel + memory + model) diagnostics in sync. Mounted
142
- // once here at the root so badges survive sidebar tab open/close.
143
- useResourceDiagnosticsSync({
144
- selectItems: (s) => s.channels,
145
- validate: validateChannel,
146
- getStored: (d) => d.byChannelId,
147
- set: (d, id, diags) => d.setChannelDiagnostics(id, diags),
148
- clear: (d, id) => d.clearChannelDiagnostics(id),
149
- });
150
- useResourceDiagnosticsSync({
151
- selectItems: (s) => s.memory,
152
- validate: validateMemory,
153
- getStored: (d) => d.byMemoryId,
154
- set: (d, id, diags) => d.setMemoryDiagnostics(id, diags),
155
- clear: (d, id) => d.clearMemoryDiagnostics(id),
156
- });
157
- useResourceDiagnosticsSync({
158
- selectItems: (s) => s.models,
159
- validate: validateModel,
160
- getStored: (d) => d.byModelId,
161
- set: (d, id, diags) => d.setModelDiagnostics(id, diags),
162
- clear: (d, id) => d.clearModelDiagnostics(id),
163
- });
164
- // Functions are a FunctionDeclaration (not a flat resource bag), so they use a
165
- // dedicated diagnostics sync.
166
- useFunctionDiagnosticsSync();
167
-
168
- // Push the embedder-supplied model catalog into the store so agent model
169
- // pickers can read it. Catalog is config (not workflow content), so this
170
- // never fires onChange.
171
- useEffect(() => {
172
- useEditorStore.getState().setAvailableModels(models ?? []);
173
- }, [models]);
174
-
175
- // Canvas tabs + functions live here because they survive canvas switches.
176
- const canvasTabs = useCanvasTabs();
177
- const functionsHook = useFunctions({ onOpenTab: canvasTabs.openTab });
178
-
179
- // Built-in validation UX. validate() presents the result itself rather than
180
- // returning it: a success toast when clean, else this dialog. Non-null = open.
181
- const [validation, setValidation] = useState<ValidationResult | null>(null);
182
-
183
- const runValidate = useCallback(() => {
184
- const result = validateWorkflowState(readStateFromStores());
185
- if (result.totalErrors === 0 && result.totalWarnings === 0) {
186
- toast({ title: i18n.t("validationPassed") });
187
- } else {
188
- setValidation(result);
189
- }
190
- }, []);
191
-
192
- // Jump to a diagnostic's target, then dismiss the dialog so it's visible.
193
- const navigateToDiagnostic = useCallback(
194
- (d: Diagnostic) => {
195
- const editor = useEditorStore.getState();
196
- // Project-scoped targets: open the matching sidebar tab AND select the item.
197
- if (d.channelId) {
198
- editor.setActiveSidebarTab("channels");
199
- editor.selectChannel(d.channelId);
200
- } else if (d.memoryId) {
201
- editor.setActiveSidebarTab("memory");
202
- editor.selectMemory(d.memoryId);
203
- } else if (d.modelId) {
204
- editor.setActiveSidebarTab("models");
205
- editor.selectModel(d.modelId);
206
- } else if (d.canvasId) {
207
- // Switch first so selectGraph targets the right canvas, then select.
208
- if (d.canvasId === MAIN_CANVAS_ID) editor.setActiveCanvas(MAIN_CANVAS_ID);
209
- else functionsHook.openFunction(d.canvasId);
210
- if (d.nodeId) editor.selectGraph([d.nodeId], []);
211
- else if (d.edgeId) editor.selectGraph([], [d.edgeId]);
212
- }
213
- setValidation(null);
214
- },
215
- [functionsHook],
216
- );
217
-
218
- // Initial load (runs once, even under StrictMode double-mount).
219
- const initialLoadDone = useRef(false);
220
- useEffect(() => {
221
- if (initialLoadDone.current) return;
222
- initialLoadDone.current = true;
223
- try {
224
- if (initialMode) useEditorStore.getState().setBuilderMode(initialMode);
225
- // No initialWorkflow → start empty; the module-level stores may still hold the
226
- // previously mounted project, so clear them rather than leaving them untouched.
227
- if (initialWorkflow) importProject(initialWorkflow);
228
- else clearProject();
229
- } catch (e) {
230
- onError?.(e instanceof Error ? e : new Error(String(e)));
231
- }
232
- // eslint-disable-next-line react-hooks/exhaustive-deps
233
- }, []);
234
-
235
- // ── Lifecycle subscriptions ────────────────────────────────────────────
236
- // Stash latest callbacks in refs so the subscription effect runs once.
237
- const onChangeRef = useRef(onChange);
238
- const onHistoryChangeRef = useRef(onHistoryChange);
239
- onChangeRef.current = onChange;
240
- onHistoryChangeRef.current = onHistoryChange;
241
-
242
- // onChange fires on any domain change. For canvas content we watch the
243
- // history middleware's `mutationCount`, which bumps on checkpoints AND
244
- // undo/redo but never on selection/drag (those go through setNodes without a
245
- // checkpoint). That makes onChange honest for undo/redo and silent on
246
- // view-state — the thing a raw store subscription can't do, since selection
247
- // lives inside the nodes array. (editorStore exposes its own `mutationCount`
248
- // for project-scoped channel/memory/model edits; watched separately below.)
249
- useEffect(() => {
250
- const subs: Array<() => void> = [];
251
- const subscribedStores = new WeakSet<CanvasStore>();
252
-
253
- function subscribeCanvas(store: CanvasStore) {
254
- if (subscribedStores.has(store)) return;
255
- subscribedStores.add(store);
256
- let prev = store.getState().mutationCount;
257
- const unsub = store.subscribe((state) => {
258
- if (state.mutationCount !== prev) {
259
- prev = state.mutationCount;
260
- onChangeRef.current?.();
261
- }
262
- });
263
- subs.push(unsub);
264
- }
265
-
266
- function subscribeAllCanvases() {
267
- for (const store of Object.values(getAllCanvasStores())) {
268
- subscribeCanvas(store);
269
- }
270
- }
271
-
272
- subscribeAllCanvases();
273
-
274
- // Canvas stores come and go (function add/delete, project load). Re-subscribe
275
- // to the new set so newly created function bodies are watched. We do NOT fire
276
- // onChange here: function add/delete/rename and all definition edits flow
277
- // through editorStore.mutationCount (setFunctions), caught by the editor
278
- // subscription below — so the change signal is covered without double-firing.
279
- const unsubRegistry = subscribeCanvasRegistryChanges(() => {
280
- subscribeAllCanvases();
281
- });
282
- subs.push(unsubRegistry);
283
-
284
- // Project-scoped mutations (channels, memory, models, functions).
285
- let prevEditorCount = useEditorStore.getState().mutationCount;
286
- const unsubEditor = useEditorStore.subscribe((state) => {
287
- if (state.mutationCount !== prevEditorCount) {
288
- prevEditorCount = state.mutationCount;
289
- onChangeRef.current?.();
290
- }
291
- });
292
- subs.push(unsubEditor);
293
-
294
- return () => {
295
- for (const u of subs) u();
296
- };
297
- }, []);
298
-
299
- // History-affordance subscription — emits the ACTIVE canvas's canUndo/canRedo
300
- // so host chrome can drive undo/redo buttons. Distinct from onChange: a tab
301
- // switch changes which history is active without being a domain mutation, so
302
- // it must update buttons without marking the document dirty.
303
- useEffect(() => {
304
- let prevCanUndo: boolean | null = null;
305
- let prevCanRedo: boolean | null = null;
306
- let unsubActive: (() => void) | null = null;
307
-
308
- const emit = () => {
309
- const store = getOrCreateCanvasStore(useEditorStore.getState().activeCanvasId);
310
- const canUndo = store.canUndo();
311
- const canRedo = store.canRedo();
312
- if (canUndo === prevCanUndo && canRedo === prevCanRedo) return;
313
- prevCanUndo = canUndo;
314
- prevCanRedo = canRedo;
315
- onHistoryChangeRef.current?.({ canUndo, canRedo });
316
- };
317
-
318
- // Bind to the current active canvas (a) and emit. Re-run on tab switch (b)
319
- // and on store-instance rebuilds from load/clear (c) — both can change which
320
- // store, or store object, is active under us.
321
- const bindActive = () => {
322
- unsubActive?.();
323
- unsubActive = getOrCreateCanvasStore(useEditorStore.getState().activeCanvasId).subscribe(emit);
324
- emit();
325
- };
326
-
327
- bindActive();
328
-
329
- let prevActive = useEditorStore.getState().activeCanvasId;
330
- const unsubEditor = useEditorStore.subscribe((state) => {
331
- if (state.activeCanvasId !== prevActive) {
332
- prevActive = state.activeCanvasId;
333
- bindActive(); // (b) tab switch
334
- }
335
- });
336
- const unsubRegistry = subscribeCanvasRegistryChanges(bindActive); // (c) load/clear rebuild
337
-
338
- return () => {
339
- unsubActive?.();
340
- unsubEditor();
341
- unsubRegistry();
342
- };
343
- }, []);
344
-
345
- // ── Imperative handle ─────────────────────────────────────────────────
346
- useImperativeHandle(
347
- ref,
348
- (): WorkflowBuilderHandle => ({
349
- loadWorkflow: (workflow) => {
350
- try {
351
- importProject(workflow);
352
- } catch (e) {
353
- onError?.(e instanceof Error ? e : new Error(String(e)));
354
- }
355
- },
356
- exportWorkflow: () => exportProject(),
357
- clear: () => clearProject(),
358
- setMode: (mode) => useEditorStore.getState().setBuilderMode(mode),
359
- getMode: () => useEditorStore.getState().builderMode,
360
- validate: runValidate,
361
- undo: () => getOrCreateCanvasStore(useEditorStore.getState().activeCanvasId).undo(),
362
- redo: () => getOrCreateCanvasStore(useEditorStore.getState().activeCanvasId).redo(),
363
- setDebugPhase: (phase) => useDebugStore.getState().setPhase(phase),
364
- }),
365
- [importProject, exportProject, clearProject, onError, runValidate],
366
- );
367
-
368
- // I18nextProvider scopes the builder's PRIVATE i18n instance to this subtree,
369
- // so the useTranslation() consumers read it (never the host's i18next).
370
- //
371
- // The `fh-builder` root carries the builder's OWN base look (font,
372
- // text color, antialiasing) on its own element. The builder no longer styles
373
- // the host's <body> — the host owns the page. `h-full w-full` makes the
374
- // builder fill whatever container it's mounted in; it never assumes the
375
- // viewport. TooltipProvider + Toaster live inside the package so the embedder
376
- // doesn't need to know we use Radix tooltips or shadcn toasts internally.
377
- return (
378
- <I18nextProvider i18n={i18n}>
379
- <TooltipProvider delayDuration={300}>
380
- <div className="fh-builder h-full w-full bg-background text-foreground font-sans antialiased">
381
- <BuilderLayout
382
- functions={functionsHook.functions}
383
- onOpenFunction={functionsHook.openFunction}
384
- onCreateFunction={functionsHook.createFunction}
385
- canvasTabs={canvasTabs.tabs}
386
- onCanvasTabChange={canvasTabs.setActiveTabId}
387
- onCanvasTabClose={canvasTabs.closeTab}
388
- onCanvasTabReorder={canvasTabs.reorderTabs}
389
- onTestNode={onTestNode}
390
- onDebugStep={onDebugStep}
391
- />
392
- <Toaster />
393
- {validation && (
394
- <ValidationDialog
395
- open
396
- onOpenChange={(o) => {
397
- if (!o) setValidation(null);
398
- }}
399
- validation={validation}
400
- onSelectDiagnostic={navigateToDiagnostic}
401
- />
402
- )}
403
- </div>
404
- </TooltipProvider>
405
- </I18nextProvider>
406
- );
407
- },
408
- );
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // Copyright (c) 2026 ForestHub. All rights reserved.
3
+ // For commercial licensing, contact root@foresthub.ai
4
+
5
+ import type { ApiWorkflow } from "@foresthubai/workflow-core/workflow";
6
+ import type { ModelInfo } from "@foresthubai/workflow-core/model";
7
+ import {
8
+ validateWorkflowState,
9
+ validateChannel,
10
+ validateMemory,
11
+ validateModel,
12
+ type Diagnostic,
13
+ type ValidationResult,
14
+ } from "@foresthubai/workflow-core/diagnostics";
15
+ import { forwardRef, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useRef, useState } from "react";
16
+ import { I18nextProvider } from "react-i18next";
17
+
18
+ import i18n from "./i18n";
19
+ import { toast } from "./hooks/use-toast";
20
+ import ValidationDialog from "./dialogs/ValidationDialog";
21
+ import { BuilderLayout } from "./BuilderLayout";
22
+ import { TooltipProvider } from "./components/ui/tooltip";
23
+ import { Toaster } from "./components/ui/toaster";
24
+ import { useCanvasTabs } from "./hooks/useCanvasTabs";
25
+ import { useResourceDiagnosticsSync } from "./hooks/useResourceDiagnosticsSync";
26
+ import { useFunctionDiagnosticsSync } from "./hooks/useFunctionDiagnosticsSync";
27
+ import { useSuppressThemeTransition } from "./hooks/useSuppressThemeTransition";
28
+ import { useFunctions } from "./hooks/useFunctions";
29
+ import { useWorkflowSerialization, readStateFromStores } from "./hooks/useWorkflowSerialization";
30
+ import {
31
+ getAllCanvasStores,
32
+ getOrCreateCanvasStore,
33
+ subscribeCanvasRegistryChanges,
34
+ MAIN_CANVAS_ID,
35
+ type CanvasStore,
36
+ } from "./stores/canvasStore";
37
+ import { useDebugStore, type DebugSessionPhase } from "./stores/debugStore";
38
+ import { useEditorStore } from "./stores/editorStore";
39
+
40
+ import type { BuilderMode } from "./mode";
41
+
42
+ // ============================================================================
43
+ // Public contract
44
+ // ============================================================================
45
+
46
+ export interface WorkflowBuilderProps {
47
+ /** Workflow loaded on mount. If none is provided, an empty workflow is created. */
48
+ initialWorkflow?: ApiWorkflow;
49
+ /** Builder mode on mount. Defaults to { type: "edit" }. */
50
+ initialMode?: BuilderMode;
51
+ /**
52
+ * Static model catalog — the models the llmproxy supports. Shown as the
53
+ * built-in options in agent model pickers. Self-hosted/custom models are
54
+ * declared in the Models tab instead. Defaults to [] (empty dropdown).
55
+ */
56
+ models?: ModelInfo[];
57
+ /**
58
+ * UI language (e.g. "en", "de"). The host owns locale; the builder follows.
59
+ * Defaults to "en". The builder never auto-detects language.
60
+ */
61
+ language?: string;
62
+
63
+ // ── Embedder-fulfilled actions (builder asks, embedder does) ──
64
+ /** A node requested embedder-side testing (e.g. Agent "Test" button). */
65
+ onTestNode?: (nodeId: string) => void;
66
+ /** Step request from the in-builder debug panel — embedder forwards to the engine. */
67
+ onDebugStep?: (nodeId?: string) => void;
68
+
69
+ // ── Lifecycle events ──
70
+ /**
71
+ * Fires after any USER domain-state mutation. Pull current state via
72
+ * handle.exportWorkflow(). Programmatic loads (handle.loadWorkflow /
73
+ * handle.clear) do NOT fire this hosts can reset their dirty flag right
74
+ * after those calls without guarding against echoes.
75
+ */
76
+ onChange?: () => void;
77
+ /**
78
+ * Undo/redo availability for the ACTIVE canvas changed — on history mutation,
79
+ * undo/redo, or a tab switch (each canvas has its own history). For wiring host
80
+ * undo/redo buttons.
81
+ */
82
+ onHistoryChange?: (state: { canUndo: boolean; canRedo: boolean }) => void;
83
+ /** Unexpected error during builder operations (e.g. failed load). */
84
+ onError?: (error: Error) => void;
85
+ }
86
+
87
+ export interface WorkflowBuilderHandle {
88
+ // State I/O
89
+ loadWorkflow: (workflow: ApiWorkflow) => void;
90
+ exportWorkflow: () => ApiWorkflow;
91
+ clear: () => void;
92
+
93
+ // Mode (replaces preview/debug entry-point props)
94
+ setMode: (mode: BuilderMode) => void;
95
+ getMode: () => BuilderMode;
96
+
97
+ // Initiate the in-builder validation process which will either show the validation dialog or a toast if clean.
98
+ validate: () => void;
99
+
100
+ // History (so embedder chrome can wire undo/redo buttons)
101
+ undo: () => void;
102
+ redo: () => void;
103
+
104
+ // Debug (embedder pushes engine events for visualization)
105
+ setDebugPhase: (phase: DebugSessionPhase) => void;
106
+ }
107
+
108
+ // ============================================================================
109
+ // Component — owns tabs/functions/dialogs; exposes the handle; delegates
110
+ // layout/canvas rendering to BuilderLayout.
111
+ // ============================================================================
112
+
113
+ export const WorkflowBuilder = forwardRef<WorkflowBuilderHandle, WorkflowBuilderProps>(
114
+ function WorkflowBuilder(props, ref) {
115
+ const {
116
+ initialWorkflow,
117
+ initialMode,
118
+ models,
119
+ language,
120
+ onTestNode,
121
+ onDebugStep,
122
+ onChange,
123
+ onHistoryChange,
124
+ onError,
125
+ } = props;
126
+
127
+ // Host drives locale. useLayoutEffect (not useEffect) so the language is set
128
+ // before paint — mounting with language="de" shows German on the first frame
129
+ // rather than flashing English. changeLanguage is sync here (bundled resources).
130
+ useLayoutEffect(() => {
131
+ if (language && i18n.language !== language) i18n.changeLanguage(language);
132
+ }, [language]);
133
+
134
+ const { importProject, exportProject, clearProject } = useWorkflowSerialization();
135
+
136
+ // Color-mode toggles should snap, not fade see hook docs.
137
+ useSuppressThemeTransition();
138
+
139
+ // Keep project-scoped (channel + memory + model) diagnostics in sync. Mounted
140
+ // once here at the root so badges survive sidebar tab open/close.
141
+ useResourceDiagnosticsSync({
142
+ selectItems: (s) => s.channels,
143
+ validate: validateChannel,
144
+ getStored: (d) => d.byChannelId,
145
+ set: (d, id, diags) => d.setChannelDiagnostics(id, diags),
146
+ clear: (d, id) => d.clearChannelDiagnostics(id),
147
+ });
148
+ useResourceDiagnosticsSync({
149
+ selectItems: (s) => s.memory,
150
+ validate: validateMemory,
151
+ getStored: (d) => d.byMemoryId,
152
+ set: (d, id, diags) => d.setMemoryDiagnostics(id, diags),
153
+ clear: (d, id) => d.clearMemoryDiagnostics(id),
154
+ });
155
+ useResourceDiagnosticsSync({
156
+ selectItems: (s) => s.models,
157
+ validate: validateModel,
158
+ getStored: (d) => d.byModelId,
159
+ set: (d, id, diags) => d.setModelDiagnostics(id, diags),
160
+ clear: (d, id) => d.clearModelDiagnostics(id),
161
+ });
162
+ // Functions are a FunctionDeclaration (not a flat resource bag), so they use a
163
+ // dedicated diagnostics sync.
164
+ useFunctionDiagnosticsSync();
165
+
166
+ // Push the embedder-supplied model catalog into the store so agent model
167
+ // pickers can read it. Catalog is config (not workflow content), so this
168
+ // never fires onChange.
169
+ useEffect(() => {
170
+ useEditorStore.getState().setAvailableModels(models ?? []);
171
+ }, [models]);
172
+
173
+ // Canvas tabs + functions live here because they survive canvas switches.
174
+ const canvasTabs = useCanvasTabs();
175
+ const functionsHook = useFunctions({ onOpenTab: canvasTabs.openTab });
176
+
177
+ // Built-in validation UX. validate() presents the result itself rather than
178
+ // returning it: a success toast when clean, else this dialog. Non-null = open.
179
+ const [validation, setValidation] = useState<ValidationResult | null>(null);
180
+
181
+ const runValidate = useCallback(() => {
182
+ const result = validateWorkflowState(readStateFromStores());
183
+ if (result.totalErrors === 0 && result.totalWarnings === 0) {
184
+ toast({ title: i18n.t("validationPassed") });
185
+ } else {
186
+ setValidation(result);
187
+ }
188
+ }, []);
189
+
190
+ // Jump to a diagnostic's target, then dismiss the dialog so it's visible.
191
+ const navigateToDiagnostic = useCallback(
192
+ (d: Diagnostic) => {
193
+ const editor = useEditorStore.getState();
194
+ // Project-scoped targets: open the matching sidebar tab AND select the item.
195
+ if (d.channelId) {
196
+ editor.setActiveSidebarTab("channels");
197
+ editor.selectChannel(d.channelId);
198
+ } else if (d.memoryId) {
199
+ editor.setActiveSidebarTab("memory");
200
+ editor.selectMemory(d.memoryId);
201
+ } else if (d.modelId) {
202
+ editor.setActiveSidebarTab("models");
203
+ editor.selectModel(d.modelId);
204
+ } else if (d.canvasId) {
205
+ // Switch first so selectGraph targets the right canvas, then select.
206
+ if (d.canvasId === MAIN_CANVAS_ID) editor.setActiveCanvas(MAIN_CANVAS_ID);
207
+ else functionsHook.openFunction(d.canvasId);
208
+ if (d.nodeId) editor.selectGraph([d.nodeId], []);
209
+ else if (d.edgeId) editor.selectGraph([], [d.edgeId]);
210
+ }
211
+ setValidation(null);
212
+ },
213
+ [functionsHook],
214
+ );
215
+
216
+ // Initial load (runs once, even under StrictMode double-mount).
217
+ const initialLoadDone = useRef(false);
218
+ useEffect(() => {
219
+ if (initialLoadDone.current) return;
220
+ initialLoadDone.current = true;
221
+ try {
222
+ if (initialMode) useEditorStore.getState().setBuilderMode(initialMode);
223
+ // No initialWorkflow → start empty; the module-level stores may still hold the
224
+ // previously mounted project, so clear them rather than leaving them untouched.
225
+ if (initialWorkflow) importProject(initialWorkflow);
226
+ else clearProject();
227
+ } catch (e) {
228
+ onError?.(e instanceof Error ? e : new Error(String(e)));
229
+ }
230
+ // eslint-disable-next-line react-hooks/exhaustive-deps
231
+ }, []);
232
+
233
+ // ── Lifecycle subscriptions ────────────────────────────────────────────
234
+ // Stash latest callbacks in refs so the subscription effect runs once.
235
+ const onChangeRef = useRef(onChange);
236
+ const onHistoryChangeRef = useRef(onHistoryChange);
237
+ onChangeRef.current = onChange;
238
+ onHistoryChangeRef.current = onHistoryChange;
239
+
240
+ // onChange means "the USER changed the document". Programmatic loads
241
+ // (handle.loadWorkflow / handle.clear) mutate the same stores, so their
242
+ // subscription callbacks which zustand runs synchronously inside the
243
+ // store writes are muted while this flag is up. Without it every host
244
+ // sees phantom onChange events on load and has to guard its dirty flag.
245
+ const suppressChangeRef = useRef(false);
246
+
247
+ // onChange fires on any domain change. For canvas content we watch the
248
+ // history middleware's `mutationCount`, which bumps on checkpoints AND
249
+ // undo/redo but never on selection/drag (those go through setNodes without a
250
+ // checkpoint). That makes onChange honest for undo/redo and silent on
251
+ // view-state the thing a raw store subscription can't do, since selection
252
+ // lives inside the nodes array. (editorStore exposes its own `mutationCount`
253
+ // for project-scoped channel/memory/model edits; watched separately below.)
254
+ useEffect(() => {
255
+ const subs: Array<() => void> = [];
256
+ const subscribedStores = new WeakSet<CanvasStore>();
257
+
258
+ function subscribeCanvas(store: CanvasStore) {
259
+ if (subscribedStores.has(store)) return;
260
+ subscribedStores.add(store);
261
+ let prev = store.getState().mutationCount;
262
+ const unsub = store.subscribe((state) => {
263
+ if (state.mutationCount !== prev) {
264
+ prev = state.mutationCount; // always track, even when muted
265
+ if (!suppressChangeRef.current) onChangeRef.current?.();
266
+ }
267
+ });
268
+ subs.push(unsub);
269
+ }
270
+
271
+ function subscribeAllCanvases() {
272
+ for (const store of Object.values(getAllCanvasStores())) {
273
+ subscribeCanvas(store);
274
+ }
275
+ }
276
+
277
+ subscribeAllCanvases();
278
+
279
+ // Canvas stores come and go (function add/delete, project load). Re-subscribe
280
+ // to the new set so newly created function bodies are watched. We do NOT fire
281
+ // onChange here: function add/delete/rename and all definition edits flow
282
+ // through editorStore.mutationCount (setFunctions), caught by the editor
283
+ // subscription below — so the change signal is covered without double-firing.
284
+ const unsubRegistry = subscribeCanvasRegistryChanges(() => {
285
+ subscribeAllCanvases();
286
+ });
287
+ subs.push(unsubRegistry);
288
+
289
+ // Project-scoped mutations (channels, memory, models, functions).
290
+ let prevEditorCount = useEditorStore.getState().mutationCount;
291
+ const unsubEditor = useEditorStore.subscribe((state) => {
292
+ if (state.mutationCount !== prevEditorCount) {
293
+ prevEditorCount = state.mutationCount; // always track, even when muted
294
+ if (!suppressChangeRef.current) onChangeRef.current?.();
295
+ }
296
+ });
297
+ subs.push(unsubEditor);
298
+
299
+ return () => {
300
+ for (const u of subs) u();
301
+ };
302
+ }, []);
303
+
304
+ // History-affordance subscription emits the ACTIVE canvas's canUndo/canRedo
305
+ // so host chrome can drive undo/redo buttons. Distinct from onChange: a tab
306
+ // switch changes which history is active without being a domain mutation, so
307
+ // it must update buttons without marking the document dirty.
308
+ useEffect(() => {
309
+ let prevCanUndo: boolean | null = null;
310
+ let prevCanRedo: boolean | null = null;
311
+ let unsubActive: (() => void) | null = null;
312
+
313
+ const emit = () => {
314
+ const store = getOrCreateCanvasStore(useEditorStore.getState().activeCanvasId);
315
+ const canUndo = store.canUndo();
316
+ const canRedo = store.canRedo();
317
+ if (canUndo === prevCanUndo && canRedo === prevCanRedo) return;
318
+ prevCanUndo = canUndo;
319
+ prevCanRedo = canRedo;
320
+ onHistoryChangeRef.current?.({ canUndo, canRedo });
321
+ };
322
+
323
+ // Bind to the current active canvas (a) and emit. Re-run on tab switch (b)
324
+ // and on store-instance rebuilds from load/clear (c) — both can change which
325
+ // store, or store object, is active under us.
326
+ const bindActive = () => {
327
+ unsubActive?.();
328
+ unsubActive = getOrCreateCanvasStore(useEditorStore.getState().activeCanvasId).subscribe(emit);
329
+ emit();
330
+ };
331
+
332
+ bindActive();
333
+
334
+ let prevActive = useEditorStore.getState().activeCanvasId;
335
+ const unsubEditor = useEditorStore.subscribe((state) => {
336
+ if (state.activeCanvasId !== prevActive) {
337
+ prevActive = state.activeCanvasId;
338
+ bindActive(); // (b) tab switch
339
+ }
340
+ });
341
+ const unsubRegistry = subscribeCanvasRegistryChanges(bindActive); // (c) load/clear rebuild
342
+
343
+ return () => {
344
+ unsubActive?.();
345
+ unsubEditor();
346
+ unsubRegistry();
347
+ };
348
+ }, []);
349
+
350
+ // ── Imperative handle ─────────────────────────────────────────────────
351
+ useImperativeHandle(
352
+ ref,
353
+ (): WorkflowBuilderHandle => ({
354
+ loadWorkflow: (workflow) => {
355
+ suppressChangeRef.current = true;
356
+ try {
357
+ importProject(workflow);
358
+ } catch (e) {
359
+ onError?.(e instanceof Error ? e : new Error(String(e)));
360
+ } finally {
361
+ suppressChangeRef.current = false;
362
+ }
363
+ },
364
+ exportWorkflow: () => exportProject(),
365
+ clear: () => {
366
+ suppressChangeRef.current = true;
367
+ try {
368
+ clearProject();
369
+ } finally {
370
+ suppressChangeRef.current = false;
371
+ }
372
+ },
373
+ setMode: (mode) => useEditorStore.getState().setBuilderMode(mode),
374
+ getMode: () => useEditorStore.getState().builderMode,
375
+ validate: runValidate,
376
+ undo: () => getOrCreateCanvasStore(useEditorStore.getState().activeCanvasId).undo(),
377
+ redo: () => getOrCreateCanvasStore(useEditorStore.getState().activeCanvasId).redo(),
378
+ setDebugPhase: (phase) => useDebugStore.getState().setPhase(phase),
379
+ }),
380
+ [importProject, exportProject, clearProject, onError, runValidate],
381
+ );
382
+
383
+ // I18nextProvider scopes the builder's PRIVATE i18n instance to this subtree,
384
+ // so the useTranslation() consumers read it (never the host's i18next).
385
+ //
386
+ // The `fh-builder` root carries the builder's OWN base look (font,
387
+ // text color, antialiasing) on its own element. The builder no longer styles
388
+ // the host's <body> — the host owns the page. `h-full w-full` makes the
389
+ // builder fill whatever container it's mounted in; it never assumes the
390
+ // viewport. TooltipProvider + Toaster live inside the package so the embedder
391
+ // doesn't need to know we use Radix tooltips or shadcn toasts internally.
392
+ return (
393
+ <I18nextProvider i18n={i18n}>
394
+ <TooltipProvider delayDuration={300}>
395
+ <div className="fh-builder h-full w-full bg-background text-foreground font-sans antialiased">
396
+ <BuilderLayout
397
+ functions={functionsHook.functions}
398
+ onOpenFunction={functionsHook.openFunction}
399
+ onCreateFunction={functionsHook.createFunction}
400
+ canvasTabs={canvasTabs.tabs}
401
+ onCanvasTabChange={canvasTabs.setActiveTabId}
402
+ onCanvasTabClose={canvasTabs.closeTab}
403
+ onCanvasTabReorder={canvasTabs.reorderTabs}
404
+ onTestNode={onTestNode}
405
+ onDebugStep={onDebugStep}
406
+ />
407
+ <Toaster />
408
+ {validation && (
409
+ <ValidationDialog
410
+ open
411
+ onOpenChange={(o) => {
412
+ if (!o) setValidation(null);
413
+ }}
414
+ validation={validation}
415
+ onSelectDiagnostic={navigateToDiagnostic}
416
+ />
417
+ )}
418
+ </div>
419
+ </TooltipProvider>
420
+ </I18nextProvider>
421
+ );
422
+ },
423
+ );