@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,630 +1,634 @@
1
- import { Button } from "../components/ui/button";
2
- import { AddButton } from "../components/ui/add-button";
3
- import { ReadOnlyBanner } from "../components/ui/readonly-banner";
4
- import { DeleteButton } from "../components/ui/delete-button";
5
- import { Checkbox } from "../components/ui/checkbox";
6
- import { Input } from "../components/ui/input";
7
- import { Separator } from "../components/ui/separator";
8
- import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../components/ui/select";
9
- import { ToggleGroup, ToggleGroupItem } from "../components/ui/toggle-group";
10
- import {
11
- NodeDefinition,
12
- NodeData,
13
- OutputBinding,
14
- FunctionCallNode,
15
- getArguments,
16
- getNodeAvailableOutput,
17
- getOutputBinding,
18
- } from "@foresthubai/workflow-core/node";
19
- import type { DataType, Reference } from "@foresthubai/workflow-core";
20
- import type { StaticOutput, OutputList, OutputDeclaration } from "@foresthubai/workflow-core/parameter";
21
- import { isParameterActive, Parameter } from "@foresthubai/workflow-core/parameter";
22
- import { generateId } from "@foresthubai/workflow-core/id";
23
- import { ArrowRight, ChevronRight, Plus, RefreshCw, Trash2 } from "lucide-react";
24
- import { getOrCreateCanvasStore } from "../stores/canvasStore";
25
- import { isNodeUsedAsTool } from "@foresthubai/workflow-core/node";
26
- import { varKey, refToLookupKey } from "@foresthubai/workflow-core/variable";
27
- import type { Diagnostic } from "@foresthubai/workflow-core/diagnostics";
28
- import { Fragment, useCallback, useEffect, useMemo, useState } from "react";
29
- import { useTranslation } from "react-i18next";
30
- import ParameterEditor from "../inputs/ParameterEditor";
31
- import { useDiagnosticsStore } from "../stores/diagnosticsStore";
32
- import { useFunctionRegistry } from "../hooks/useFunctionRegistry";
33
- import { useParamErrors } from "../hooks/useParamErrors";
34
- import { buildFunctionNodeDef } from "../hooks/useNodeDefinitions";
35
- import { useEditorStore } from "../stores/editorStore";
36
- import { isReadOnly } from "../WorkflowBuilder";
37
- import { migrateFunctionCallNodes } from "../utils/migrateFunctionNodes";
38
- import { getNodeDescription } from "../utils/translation";
39
- import { useAvailableVariables } from "../hooks/useAvailableVariables";
40
-
41
- interface NodeConfigPanelProps {
42
- canvasId: string;
43
- selectedNode: NodeData;
44
- onNodeUpdate: (nodeId: string, updates: { arguments?: Record<string, unknown>; label?: string }) => void;
45
- onNodeDelete: (nodeId: string) => void;
46
- onClose: () => void;
47
- onOpenTest: (nodeId: string) => void;
48
- getNodeDef: (node: NodeData) => NodeDefinition | undefined;
49
- }
50
-
51
- export const NodeConfigPanel = ({
52
- canvasId,
53
- selectedNode,
54
- onNodeUpdate,
55
- onNodeDelete,
56
- onClose,
57
- onOpenTest,
58
- getNodeDef,
59
- }: NodeConfigPanelProps) => {
60
- const { t } = useTranslation();
61
- const readOnly = useEditorStore((s) => isReadOnly(s.builderMode));
62
-
63
- // Local state for label input to preserve cursor position
64
- const [localLabel, setLocalLabel] = useState(selectedNode.label || "");
65
- const [isFocused, setIsFocused] = useState(false);
66
- useEffect(() => {
67
- setLocalLabel(selectedNode.label || "");
68
- }, [selectedNode.id, selectedNode.label]);
69
-
70
- // Check if FunctionCall node is stale (e.g. after undo reverted migration)
71
- const functionId = selectedNode.type === "FunctionCall" ? (selectedNode as FunctionCallNode).functionInfo.id : null;
72
- const { getFunction } = useFunctionRegistry();
73
- const functionInfo = functionId ? getFunction(functionId) : undefined;
74
- const isStaleFunction = (() => {
75
- if (selectedNode.type !== "FunctionCall" || !functionInfo) return false;
76
- const functionNode = selectedNode as FunctionCallNode;
77
- return functionNode.functionInfo.version !== functionInfo.version;
78
- })();
79
-
80
- // Get node definition - for FunctionCallNodes, build from node's stored functionInfo
81
- const nodeDefinition =
82
- selectedNode.type === "FunctionCall"
83
- ? buildFunctionNodeDef({
84
- ...(selectedNode as FunctionCallNode).functionInfo,
85
- name: functionInfo?.name ?? (selectedNode as FunctionCallNode).functionInfo.name,
86
- })
87
- : getNodeDef(selectedNode);
88
- const cannotDelete = nodeDefinition?.isUnremovable ?? false;
89
-
90
- // Detect whether this node is currently used as a tool input
91
- const edges = getOrCreateCanvasStore(canvasId)((s) => s.edges);
92
- const usedAsToolInput = useMemo(() => isNodeUsedAsTool(selectedNode.id, selectedNode, edges), [selectedNode, edges]);
93
-
94
- // Read per-parameter error state from diagnostics store
95
- const nodeDiags = useDiagnosticsStore((s) => s.byNodeId[selectedNode.id]);
96
- const paramErrors = useParamErrors(nodeDiags);
97
-
98
- if (!nodeDefinition) {
99
- return (
100
- <div className="p-4">
101
- <p className="text-sm text-muted-foreground">{t("unknownNodeType", { type: selectedNode.type })}</p>
102
- </div>
103
- );
104
- }
105
- const allArguments = getArguments(selectedNode);
106
- const parameters = nodeDefinition.parameters.filter((p) => isParameterActive(p, allArguments, usedAsToolInput));
107
- // OutputsSection self-hides when there's nothing to render; gate on "has any output defined".
108
- const hasAnyOutputs = (nodeDefinition.outputs ?? []).length > 0;
109
-
110
- return (
111
- <div className="p-4">
112
- <div className="space-y-4">
113
- <div className="flex items-center justify-between gap-2">
114
- <div className="flex-1 min-w-0">
115
- <div className="group flex items-center gap-1.5 rounded-md border border-transparent px-1.5 -mx-1.5 hover:border-input focus-within:border-input transition-colors">
116
- <input
117
- type="text"
118
- title={t("nodeLabel")}
119
- className="font-semibold text-lg bg-transparent w-full outline-none cursor-text py-0.5"
120
- value={isFocused ? localLabel : localLabel || nodeDefinition.label}
121
- readOnly={readOnly}
122
- onFocus={() => {
123
- if (readOnly) return;
124
- setIsFocused(true);
125
- if (!localLabel) {
126
- setLocalLabel(nodeDefinition.label);
127
- }
128
- }}
129
- onBlur={() => {
130
- setIsFocused(false);
131
- if (!localLabel || localLabel === nodeDefinition.label) {
132
- setLocalLabel("");
133
- onNodeUpdate(selectedNode.id, { label: undefined });
134
- }
135
- }}
136
- onChange={(e) => {
137
- setLocalLabel(e.target.value);
138
- onNodeUpdate(selectedNode.id, { label: e.target.value });
139
- }}
140
- />
141
- </div>
142
- <p className="text-sm text-muted-foreground">{getNodeDescription(t, nodeDefinition)}</p>
143
- </div>
144
- <Button variant="ghost" size="icon" className="shrink-0" onClick={onClose}>
145
- <ChevronRight className="h-4 w-4" />
146
- </Button>
147
- </div>
148
-
149
- {readOnly && <ReadOnlyBanner />}
150
-
151
- {parameters.length > 0 && (
152
- <>
153
- <Separator />
154
- <div className={`space-y-3 ${readOnly ? "pointer-events-none opacity-60" : ""}`}>
155
- {parameters.map((param: Parameter) => (
156
- <ParameterEditor
157
- canvasId={canvasId}
158
- key={param.id}
159
- parameter={param}
160
- value={allArguments[param.id]}
161
- allArguments={allArguments}
162
- onChange={(value) => onNodeUpdate(selectedNode.id, { arguments: { [param.id]: value } })}
163
- errors={paramErrors.get(param.id)}
164
- translationPrefix={`nodes.${selectedNode.type}`}
165
- />
166
- ))}
167
- </div>
168
- </>
169
- )}
170
-
171
- {!readOnly && isStaleFunction && (
172
- <>
173
- <Separator />
174
- <Button
175
- variant="outline"
176
- className="w-full border-warning text-warning hover:bg-warning/10"
177
- onClick={() => migrateFunctionCallNodes()}
178
- >
179
- <RefreshCw className="w-4 h-4 mr-2" />
180
- {t("updateToLatestDefinition")}
181
- </Button>
182
- </>
183
- )}
184
-
185
- {!readOnly && hasAnyOutputs && !usedAsToolInput && (
186
- <>
187
- <Separator />
188
- <OutputsSection
189
- canvasId={canvasId}
190
- node={selectedNode}
191
- nodeDefinition={nodeDefinition}
192
- onNodeUpdate={onNodeUpdate}
193
- nodeDiags={nodeDiags}
194
- />
195
- </>
196
- )}
197
-
198
- {!readOnly && selectedNode.type === "Agent" && (
199
- <>
200
- <Separator />
201
- <Button variant="outline" className="w-full" onClick={() => onOpenTest(selectedNode.id)}>
202
- {t("testAgent")}
203
- </Button>
204
- </>
205
- )}
206
-
207
- {!readOnly && !cannotDelete && (
208
- <>
209
- <Separator />
210
- <DeleteButton onClick={() => onNodeDelete(selectedNode.id)}>{t("deleteNode")}</DeleteButton>
211
- </>
212
- )}
213
- </div>
214
- </div>
215
- );
216
- };
217
-
218
- // ============================================================================
219
- // Outputs Section
220
- // ============================================================================
221
- //
222
- // Single unified section for all of a node's outputs. Renders under one "OUTPUTS"
223
- // header with sequential rows — no sub-sections, no per-list separator. Two row
224
- // shapes live side by side here:
225
- //
226
- // 1. Static output row — rows for NodeDefinition.outputs.type === "static"
227
- // (FunctionCall returns are just StaticOutputsthey
228
- // flow through this same path). Active checkbox toggles
229
- // binding.active; mode toggle picks emit vs. assign.
230
- // 2. List declaration row — rows for each entry in an OutputList's backing array
231
- // (mode: emit/assign, own dataType selector, own delete button)
232
- //
233
- // List outputs also render an "+ Add" button after their entries so new declarations
234
- // can be appended. Errors on any row (static or list) apply a destructive ring using
235
- // the same `outputId` key diagnostics.ts produces.
236
-
237
- const DATA_TYPES: DataType[] = ["int", "float", "bool", "string"];
238
-
239
- const DATA_TYPE_LABELS: Record<DataType, string> = {
240
- int: "int",
241
- float: "float",
242
- bool: "bool",
243
- string: "string",
244
- };
245
-
246
- /** Stable synthetic outputId used to key list-entry diagnostics (matches diagnostics.ts). */
247
- function listEntryOutputId(listId: string, index: number): string {
248
- return `${listId}[${index}]`;
249
- }
250
-
251
- /** Convert an AvailableVariable into a canonical Reference (srcId + varId). */
252
- function availableVarToRef(
253
- v:
254
- | { kind: "node"; nodeId: string; outputId: string }
255
- | { kind: "declared"; uid: string }
256
- | { kind: "fnarg"; uid: string },
257
- ): Reference {
258
- if (v.kind === "node") return { srcId: v.nodeId, varId: v.outputId };
259
- if (v.kind === "declared") return { srcId: "declared", varId: v.uid };
260
- return { srcId: "fnarg", varId: v.uid };
261
- }
262
-
263
- function OutputsSection({
264
- canvasId,
265
- node,
266
- nodeDefinition,
267
- onNodeUpdate,
268
- nodeDiags,
269
- }: {
270
- canvasId: string;
271
- node: NodeData;
272
- nodeDefinition: NodeDefinition;
273
- onNodeUpdate: (nodeId: string, updates: { arguments?: Record<string, unknown> }) => void;
274
- nodeDiags: Diagnostic[] | undefined;
275
- }) {
276
- const { t } = useTranslation();
277
- const { list: availableVars } = useAvailableVariables(canvasId);
278
- const availableOutput = useMemo(() => getNodeAvailableOutput(node), [node]);
279
-
280
- const staticOutputs = useMemo(
281
- () => (nodeDefinition.outputs ?? []).filter((o): o is StaticOutput => o.type === "static"),
282
- [nodeDefinition],
283
- );
284
- const listOutputs = useMemo(
285
- () => (nodeDefinition.outputs ?? []).filter((o): o is OutputList => o.type === "list"),
286
- [nodeDefinition],
287
- );
288
-
289
- // Collect diagnostics keyed by outputId so rows can look themselves up.
290
- const outputErrors = useMemo(() => {
291
- const map = new Map<string, string[]>();
292
- if (!nodeDiags) return map;
293
- for (const d of nodeDiags) {
294
- if (d.outputId && d.severity === "error") {
295
- const arr = map.get(d.outputId);
296
- if (arr) arr.push(d.message);
297
- else map.set(d.outputId, [d.message]);
298
- }
299
- }
300
- return map;
301
- }, [nodeDiags]);
302
-
303
- /**
304
- * Write a binding for a static output (or FunctionCall return). List entries
305
- * have their own update path via `replaceListEntry`.
306
- */
307
- const updateStaticBinding = useCallback(
308
- (key: string, binding: OutputBinding) => {
309
- onNodeUpdate(node.id, { arguments: { [key]: binding } });
310
- },
311
- [node.id, onNodeUpdate],
312
- );
313
-
314
- const writeListEntries = useCallback(
315
- (listId: string, next: OutputDeclaration[]) => onNodeUpdate(node.id, { arguments: { [listId]: next } }),
316
- [node.id, onNodeUpdate],
317
- );
318
-
319
- // Filter available vars to matching dataType, excluding this node's own outputs
320
- // (a node can't assign its own output back into itself).
321
- const filterCompatible = useCallback(
322
- (dataType: DataType) =>
323
- availableVars.filter((v) => v.dataType === dataType && !("nodeId" in v && v.nodeId === node.id)),
324
- [availableVars, node.id],
325
- );
326
-
327
- // Any rows to show? Early-exit so the parent panel doesn't render an empty section.
328
- if (staticOutputs.length === 0 && listOutputs.length === 0) return null;
329
-
330
- // --- Row renderer: static output / FunctionCall return ------------------------------
331
- // UX shape:
332
- // ┌─ card ────────────────────────────────────────┐
333
- // │ [☑] label dataType │
334
- // │ [emit | assign] [ name OR variable picker ]
335
- // └────────────────────────────────────────────────┘
336
- // Checkbox sits inline with the label (so the card width matches list-entry rows
337
- // which have no checkbox). Toggles `binding.active`; mode/name/target are kept as
338
- // draft state when inactive so off→on round-trips identically. Body is dimmed and
339
- // pointer-event-disabled while inactive.
340
- const renderStaticRow = (key: string, output: { name: string; dataType: DataType }, displayLabel: string) => {
341
- const binding = getOutputBinding(node, key) ?? ({ active: true, mode: "emit", name: output.name } as OutputBinding);
342
- const compatibleVars = filterCompatible(output.dataType);
343
- const errors = outputErrors.get(key);
344
- const hasError = !!errors?.length;
345
- const enabled = binding.active;
346
-
347
- const setEnabled = (next: boolean) => {
348
- if (next === enabled) return;
349
- updateStaticBinding(key, { ...binding, active: next });
350
- };
351
- const setMode = (mode: "emit" | "assign") => {
352
- if (mode === binding.mode) return;
353
- if (mode === "emit") updateStaticBinding(key, { active: binding.active, mode: "emit", name: output.name });
354
- else updateStaticBinding(key, { active: binding.active, mode: "assign", target: { srcId: "", varId: "" } });
355
- };
356
-
357
- return (
358
- <div
359
- key={key}
360
- className={`rounded-lg bg-card shadow-sm border p-2 space-y-2 transition-all hover:shadow-md ${hasError ? "border-destructive ring-1 ring-destructive" : "border-border"}`}
361
- >
362
- <div className="flex items-center gap-2">
363
- <Checkbox
364
- checked={enabled}
365
- onCheckedChange={(c) => setEnabled(c === true)}
366
- aria-label={t("outputBinding.enable", "Enable output")}
367
- className="shrink-0"
368
- />
369
- <span className={`text-xs font-medium flex-1 truncate ${enabled ? "" : "opacity-50"}`}>{displayLabel}</span>
370
- <span className={`text-xs text-muted-foreground ${enabled ? "" : "opacity-50"}`}>
371
- {DATA_TYPE_LABELS[output.dataType]}
372
- </span>
373
- </div>
374
- <div className={enabled ? "" : "opacity-50"}>
375
- <div className={`flex items-center gap-2 ${enabled ? "" : "pointer-events-none"}`}>
376
- <ToggleGroup
377
- type="single"
378
- value={binding.mode}
379
- onValueChange={(v) => v && setMode(v as "emit" | "assign")}
380
- className="gap-0"
381
- >
382
- <ToggleGroupItem
383
- value="emit"
384
- size="sm"
385
- variant="outline"
386
- className="h-7 w-7 p-0 rounded-r-none"
387
- aria-label={t("outputBinding.emit", "Emit")}
388
- title={t("outputBinding.emit", "Emit")}
389
- >
390
- <Plus className="w-3.5 h-3.5" />
391
- </ToggleGroupItem>
392
- <ToggleGroupItem
393
- value="assign"
394
- size="sm"
395
- variant="outline"
396
- className="h-7 w-7 p-0 rounded-l-none -ml-px"
397
- aria-label={t("outputBinding.assign", "Assign")}
398
- title={t("outputBinding.assign", "Assign")}
399
- >
400
- <ArrowRight className="w-3.5 h-3.5" />
401
- </ToggleGroupItem>
402
- </ToggleGroup>
403
-
404
- {binding.mode === "emit" ? (
405
- <Input
406
- className="h-7 text-xs flex-1"
407
- value={binding.name}
408
- disabled={!enabled}
409
- onChange={(e) =>
410
- updateStaticBinding(key, { active: binding.active, mode: "emit", name: e.target.value })
411
- }
412
- />
413
- ) : compatibleVars.length > 0 ? (
414
- <Select
415
- value={binding.target.srcId ? refToLookupKey(binding.target) : ""}
416
- onValueChange={(lookupKey) => {
417
- const v = availableVars.find((av) => varKey(av) === lookupKey);
418
- if (!v) return;
419
- updateStaticBinding(key, { active: binding.active, mode: "assign", target: availableVarToRef(v) });
420
- }}
421
- disabled={!enabled}
422
- >
423
- <SelectTrigger className="h-7 text-xs flex-1">
424
- <SelectValue placeholder={t("outputBinding.selectVariable", "Select variable...")} />
425
- </SelectTrigger>
426
- <SelectContent>
427
- {compatibleVars.map((v) => (
428
- <SelectItem key={varKey(v)} value={varKey(v)}>
429
- {v.name}
430
- </SelectItem>
431
- ))}
432
- </SelectContent>
433
- </Select>
434
- ) : (
435
- <span className="text-xs text-muted-foreground italic flex-1">
436
- {t("outputBinding.noCompatibleVariables", "No compatible variables")}
437
- </span>
438
- )}
439
- </div>
440
- {hasError && (
441
- <div className="space-y-0.5">
442
- {errors.map((msg, i) => (
443
- <p key={i} className="text-xs text-destructive">
444
- {msg}
445
- </p>
446
- ))}
447
- </div>
448
- )}
449
- </div>
450
- </div>
451
- );
452
- };
453
-
454
- // --- Row renderer: list declaration entry ------------------------------------------
455
- const renderListEntryRow = (listId: string, entries: OutputDeclaration[], index: number) => {
456
- const entry = entries[index];
457
- if (!entry) return null;
458
- const compatibleVars = filterCompatible(entry.dataType);
459
- const outputId = listEntryOutputId(listId, index);
460
- const errors = outputErrors.get(outputId);
461
- const hasError = !!errors?.length;
462
-
463
- const replace = (next: OutputDeclaration) =>
464
- writeListEntries(
465
- listId,
466
- entries.map((e, i) => (i === index ? next : e)),
467
- );
468
- const remove = () =>
469
- writeListEntries(
470
- listId,
471
- entries.filter((_, i) => i !== index),
472
- );
473
- // Mode flip preserves name + dataType — the user already typed/picked them.
474
- // emit↔assign only swaps the trailing payload (uid vs target).
475
- const changeMode = (newMode: "emit" | "assign") => {
476
- if (entry.mode === newMode) return;
477
- if (newMode === "emit") {
478
- replace({ mode: "emit", uid: generateId(), name: entry.name, dataType: entry.dataType });
479
- } else {
480
- replace({ mode: "assign", name: entry.name, dataType: entry.dataType, target: { srcId: "", varId: "" } });
481
- }
482
- };
483
- const changeName = (name: string) => replace({ ...entry, name });
484
- const changeDataType = (dt: DataType) => replace({ ...entry, dataType: dt });
485
-
486
- return (
487
- <div
488
- key={`${listId}-${entry.mode === "emit" ? entry.uid : `assign-${index}`}`}
489
- className={`rounded-lg bg-card shadow-sm border p-2 space-y-2 transition-all hover:shadow-md ${hasError ? "border-destructive ring-1 ring-destructive" : "border-border"}`}
490
- >
491
- <div className="flex items-center gap-2">
492
- {/* Row 1 mirrors OutputBinding's header: leading control (trash↔checkbox),
493
- name (editable here, label-only there), dataType (editable here, RO there). */}
494
- <Button variant="ghost" size="icon" className="h-7 w-7 shrink-0" onClick={remove}>
495
- <Trash2 className="w-3.5 h-3.5" />
496
- </Button>
497
- <Input
498
- className="h-7 text-xs flex-1"
499
- value={entry.name}
500
- placeholder={t("outputBinding.name", "Name")}
501
- onChange={(e) => changeName(e.target.value)}
502
- />
503
- <Select value={entry.dataType} onValueChange={(dt: DataType) => changeDataType(dt)}>
504
- <SelectTrigger className="h-7 text-xs w-20">
505
- <SelectValue />
506
- </SelectTrigger>
507
- <SelectContent>
508
- {DATA_TYPES.map((dt) => (
509
- <SelectItem key={dt} value={dt}>
510
- {dt}
511
- </SelectItem>
512
- ))}
513
- </SelectContent>
514
- </Select>
515
- </div>
516
- <div className="flex items-center gap-2">
517
- <ToggleGroup
518
- type="single"
519
- value={entry.mode}
520
- onValueChange={(v) => v && changeMode(v as "emit" | "assign")}
521
- className="gap-0"
522
- >
523
- <ToggleGroupItem
524
- value="emit"
525
- size="sm"
526
- variant="outline"
527
- className="h-7 w-7 p-0 rounded-r-none"
528
- aria-label={t("outputBinding.emit", "Emit")}
529
- title={t("outputBinding.emit", "Emit")}
530
- >
531
- <Plus className="w-3.5 h-3.5" />
532
- </ToggleGroupItem>
533
- <ToggleGroupItem
534
- value="assign"
535
- size="sm"
536
- variant="outline"
537
- className="h-7 w-7 p-0 rounded-l-none -ml-px"
538
- aria-label={t("outputBinding.assign", "Assign")}
539
- title={t("outputBinding.assign", "Assign")}
540
- >
541
- <ArrowRight className="w-3.5 h-3.5" />
542
- </ToggleGroupItem>
543
- </ToggleGroup>
544
- {entry.mode === "emit" ? (
545
- <span className="text-xs text-muted-foreground italic flex-1">
546
- {t("outputBinding.emitHint", "creates new variable in scope")}
547
- </span>
548
- ) : compatibleVars.length > 0 ? (
549
- <Select
550
- value={entry.target.srcId ? refToLookupKey(entry.target) : ""}
551
- onValueChange={(lookupKey) => {
552
- const v = availableVars.find((av) => varKey(av) === lookupKey);
553
- if (!v) return;
554
- replace({ ...entry, target: availableVarToRef(v) });
555
- }}
556
- >
557
- <SelectTrigger className="h-7 text-xs flex-1">
558
- <SelectValue placeholder={t("outputBinding.selectVariable", "Select variable...")} />
559
- </SelectTrigger>
560
- <SelectContent>
561
- {compatibleVars.map((v) => (
562
- <SelectItem key={varKey(v)} value={varKey(v)}>
563
- {v.name}
564
- </SelectItem>
565
- ))}
566
- </SelectContent>
567
- </Select>
568
- ) : (
569
- <span className="text-xs text-muted-foreground italic flex-1">
570
- {t("outputBinding.noCompatibleVariables", "No compatible variables")}
571
- </span>
572
- )}
573
- </div>
574
- {hasError && (
575
- <div className="space-y-0.5">
576
- {errors.map((msg, i) => (
577
- <p key={i} className="text-xs text-destructive">
578
- {msg}
579
- </p>
580
- ))}
581
- </div>
582
- )}
583
- </div>
584
- );
585
- };
586
-
587
- const addListEntry = (listId: string, entries: OutputDeclaration[]) => {
588
- const fresh: OutputDeclaration = {
589
- mode: "emit",
590
- uid: generateId(),
591
- name: `output${entries.length + 1}`,
592
- dataType: "string",
593
- };
594
- writeListEntries(listId, [...entries, fresh]);
595
- };
596
-
597
- return (
598
- <div className="space-y-2">
599
- <p className="text-xs font-medium text-muted-foreground uppercase tracking-wide">{t("outputs", "Outputs")}</p>
600
- <div className="space-y-2">
601
- {/* Static outputs from the node definition (FunctionCall returns flow here too). */}
602
- {staticOutputs.map((out) => {
603
- const output = availableOutput[out.id];
604
- if (!output) return null;
605
- return renderStaticRow(out.id, output, out.label);
606
- })}
607
-
608
- {/* List outputs — each list gets a minor subheader (its label), then its entries,
609
- then an "Add" button. The subheader also serves as the visual boundary between
610
- static outputs above and list entries below. */}
611
- {listOutputs.map((out) => {
612
- const entries =
613
- ((node.arguments as Record<string, unknown>)[out.id] as OutputDeclaration[] | undefined) ?? [];
614
- return (
615
- <Fragment key={out.id}>
616
- <div className="flex items-center gap-2 pt-1">
617
- <span className="text-[11px] font-medium text-muted-foreground">{out.label}</span>
618
- <div className="flex-1 h-px bg-border/60" />
619
- </div>
620
- {entries.map((_, index) => renderListEntryRow(out.id, entries, index))}
621
- <AddButton onClick={() => addListEntry(out.id, entries)}>
622
- {t("addOutput", { label: out.label, defaultValue: `Add ${out.label.toLowerCase()}` })}
623
- </AddButton>
624
- </Fragment>
625
- );
626
- })}
627
- </div>
628
- </div>
629
- );
630
- }
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 { Button } from "../components/ui/button";
6
+ import { AddButton } from "../components/ui/add-button";
7
+ import { ReadOnlyBanner } from "../components/ui/readonly-banner";
8
+ import { DeleteButton } from "../components/ui/delete-button";
9
+ import { Checkbox } from "../components/ui/checkbox";
10
+ import { Input } from "../components/ui/input";
11
+ import { Separator } from "../components/ui/separator";
12
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../components/ui/select";
13
+ import { ToggleGroup, ToggleGroupItem } from "../components/ui/toggle-group";
14
+ import {
15
+ NodeDefinition,
16
+ NodeData,
17
+ OutputBinding,
18
+ FunctionCallNode,
19
+ getArguments,
20
+ getNodeAvailableOutput,
21
+ getOutputBinding,
22
+ } from "@foresthubai/workflow-core/node";
23
+ import type { DataType, Reference } from "@foresthubai/workflow-core";
24
+ import type { StaticOutput, OutputList, OutputDeclaration } from "@foresthubai/workflow-core/parameter";
25
+ import { isParameterActive, Parameter } from "@foresthubai/workflow-core/parameter";
26
+ import { generateId } from "@foresthubai/workflow-core/id";
27
+ import { ArrowRight, ChevronRight, Plus, RefreshCw, Trash2 } from "lucide-react";
28
+ import { getOrCreateCanvasStore } from "../stores/canvasStore";
29
+ import { isNodeUsedAsTool } from "@foresthubai/workflow-core/node";
30
+ import { varKey, refToLookupKey } from "@foresthubai/workflow-core/variable";
31
+ import type { Diagnostic } from "@foresthubai/workflow-core/diagnostics";
32
+ import { Fragment, useCallback, useEffect, useMemo, useState } from "react";
33
+ import { useTranslation } from "react-i18next";
34
+ import ParameterEditor from "../inputs/ParameterEditor";
35
+ import { useDiagnosticsStore } from "../stores/diagnosticsStore";
36
+ import { useFunctionRegistry } from "../hooks/useFunctionRegistry";
37
+ import { useParamErrors } from "../hooks/useParamErrors";
38
+ import { buildFunctionNodeDef } from "../hooks/useNodeDefinitions";
39
+ import { useEditorStore } from "../stores/editorStore";
40
+ import { isReadOnly } from "../mode";
41
+ import { migrateFunctionCallNodes } from "../utils/migrateFunctionNodes";
42
+ import { getNodeDescription } from "../utils/translation";
43
+ import { useAvailableVariables } from "../hooks/useAvailableVariables";
44
+
45
+ interface NodeConfigPanelProps {
46
+ canvasId: string;
47
+ selectedNode: NodeData;
48
+ onNodeUpdate: (nodeId: string, updates: { arguments?: Record<string, unknown>; label?: string }) => void;
49
+ onNodeDelete: (nodeId: string) => void;
50
+ onClose: () => void;
51
+ onOpenTest: (nodeId: string) => void;
52
+ getNodeDef: (node: NodeData) => NodeDefinition | undefined;
53
+ }
54
+
55
+ export const NodeConfigPanel = ({
56
+ canvasId,
57
+ selectedNode,
58
+ onNodeUpdate,
59
+ onNodeDelete,
60
+ onClose,
61
+ onOpenTest,
62
+ getNodeDef,
63
+ }: NodeConfigPanelProps) => {
64
+ const { t } = useTranslation();
65
+ const readOnly = useEditorStore((s) => isReadOnly(s.builderMode));
66
+
67
+ // Local state for label input to preserve cursor position
68
+ const [localLabel, setLocalLabel] = useState(selectedNode.label || "");
69
+ const [isFocused, setIsFocused] = useState(false);
70
+ useEffect(() => {
71
+ setLocalLabel(selectedNode.label || "");
72
+ }, [selectedNode.id, selectedNode.label]);
73
+
74
+ // Check if FunctionCall node is stale (e.g. after undo reverted migration)
75
+ const functionId = selectedNode.type === "FunctionCall" ? (selectedNode as FunctionCallNode).functionInfo.id : null;
76
+ const { getFunction } = useFunctionRegistry();
77
+ const functionInfo = functionId ? getFunction(functionId) : undefined;
78
+ const isStaleFunction = (() => {
79
+ if (selectedNode.type !== "FunctionCall" || !functionInfo) return false;
80
+ const functionNode = selectedNode as FunctionCallNode;
81
+ return functionNode.functionInfo.version !== functionInfo.version;
82
+ })();
83
+
84
+ // Get node definition - for FunctionCallNodes, build from node's stored functionInfo
85
+ const nodeDefinition =
86
+ selectedNode.type === "FunctionCall"
87
+ ? buildFunctionNodeDef({
88
+ ...(selectedNode as FunctionCallNode).functionInfo,
89
+ name: functionInfo?.name ?? (selectedNode as FunctionCallNode).functionInfo.name,
90
+ })
91
+ : getNodeDef(selectedNode);
92
+ const cannotDelete = nodeDefinition?.isUnremovable ?? false;
93
+
94
+ // Detect whether this node is currently used as a tool input
95
+ const edges = getOrCreateCanvasStore(canvasId)((s) => s.edges);
96
+ const usedAsToolInput = useMemo(() => isNodeUsedAsTool(selectedNode.id, selectedNode, edges), [selectedNode, edges]);
97
+
98
+ // Read per-parameter error state from diagnostics store
99
+ const nodeDiags = useDiagnosticsStore((s) => s.byNodeId[selectedNode.id]);
100
+ const paramErrors = useParamErrors(nodeDiags);
101
+
102
+ if (!nodeDefinition) {
103
+ return (
104
+ <div className="p-4">
105
+ <p className="text-sm text-muted-foreground">{t("unknownNodeType", { type: selectedNode.type })}</p>
106
+ </div>
107
+ );
108
+ }
109
+ const allArguments = getArguments(selectedNode);
110
+ const parameters = nodeDefinition.parameters.filter((p) => isParameterActive(p, allArguments, usedAsToolInput));
111
+ // OutputsSection self-hides when there's nothing to render; gate on "has any output defined".
112
+ const hasAnyOutputs = (nodeDefinition.outputs ?? []).length > 0;
113
+
114
+ return (
115
+ <div className="p-4">
116
+ <div className="space-y-4">
117
+ <div className="flex items-center justify-between gap-2">
118
+ <div className="flex-1 min-w-0">
119
+ <div className="group flex items-center gap-1.5 rounded-md border border-transparent px-1.5 -mx-1.5 hover:border-input focus-within:border-input transition-colors">
120
+ <input
121
+ type="text"
122
+ title={t("nodeLabel")}
123
+ className="font-semibold text-lg bg-transparent w-full outline-none cursor-text py-0.5"
124
+ value={isFocused ? localLabel : localLabel || nodeDefinition.label}
125
+ readOnly={readOnly}
126
+ onFocus={() => {
127
+ if (readOnly) return;
128
+ setIsFocused(true);
129
+ if (!localLabel) {
130
+ setLocalLabel(nodeDefinition.label);
131
+ }
132
+ }}
133
+ onBlur={() => {
134
+ setIsFocused(false);
135
+ if (!localLabel || localLabel === nodeDefinition.label) {
136
+ setLocalLabel("");
137
+ onNodeUpdate(selectedNode.id, { label: undefined });
138
+ }
139
+ }}
140
+ onChange={(e) => {
141
+ setLocalLabel(e.target.value);
142
+ onNodeUpdate(selectedNode.id, { label: e.target.value });
143
+ }}
144
+ />
145
+ </div>
146
+ <p className="text-sm text-muted-foreground">{getNodeDescription(t, nodeDefinition)}</p>
147
+ </div>
148
+ <Button variant="ghost" size="icon" className="shrink-0" onClick={onClose}>
149
+ <ChevronRight className="h-4 w-4" />
150
+ </Button>
151
+ </div>
152
+
153
+ {readOnly && <ReadOnlyBanner />}
154
+
155
+ {parameters.length > 0 && (
156
+ <>
157
+ <Separator />
158
+ <div className={`space-y-3 ${readOnly ? "pointer-events-none opacity-60" : ""}`}>
159
+ {parameters.map((param: Parameter) => (
160
+ <ParameterEditor
161
+ canvasId={canvasId}
162
+ key={param.id}
163
+ parameter={param}
164
+ value={allArguments[param.id]}
165
+ allArguments={allArguments}
166
+ onChange={(value) => onNodeUpdate(selectedNode.id, { arguments: { [param.id]: value } })}
167
+ errors={paramErrors.get(param.id)}
168
+ translationPrefix={`nodes.${selectedNode.type}`}
169
+ />
170
+ ))}
171
+ </div>
172
+ </>
173
+ )}
174
+
175
+ {!readOnly && isStaleFunction && (
176
+ <>
177
+ <Separator />
178
+ <Button
179
+ variant="outline"
180
+ className="w-full border-warning text-warning hover:bg-warning/10"
181
+ onClick={() => migrateFunctionCallNodes()}
182
+ >
183
+ <RefreshCw className="w-4 h-4 mr-2" />
184
+ {t("updateToLatestDefinition")}
185
+ </Button>
186
+ </>
187
+ )}
188
+
189
+ {!readOnly && hasAnyOutputs && !usedAsToolInput && (
190
+ <>
191
+ <Separator />
192
+ <OutputsSection
193
+ canvasId={canvasId}
194
+ node={selectedNode}
195
+ nodeDefinition={nodeDefinition}
196
+ onNodeUpdate={onNodeUpdate}
197
+ nodeDiags={nodeDiags}
198
+ />
199
+ </>
200
+ )}
201
+
202
+ {!readOnly && selectedNode.type === "Agent" && (
203
+ <>
204
+ <Separator />
205
+ <Button variant="outline" className="w-full" onClick={() => onOpenTest(selectedNode.id)}>
206
+ {t("testAgent")}
207
+ </Button>
208
+ </>
209
+ )}
210
+
211
+ {!readOnly && !cannotDelete && (
212
+ <>
213
+ <Separator />
214
+ <DeleteButton onClick={() => onNodeDelete(selectedNode.id)}>{t("deleteNode")}</DeleteButton>
215
+ </>
216
+ )}
217
+ </div>
218
+ </div>
219
+ );
220
+ };
221
+
222
+ // ============================================================================
223
+ // Outputs Section
224
+ // ============================================================================
225
+ //
226
+ // Single unified section for all of a node's outputs. Renders under one "OUTPUTS"
227
+ // header with sequential rowsno sub-sections, no per-list separator. Two row
228
+ // shapes live side by side here:
229
+ //
230
+ // 1. Static output row — rows for NodeDefinition.outputs.type === "static"
231
+ // (FunctionCall returns are just StaticOutputs they
232
+ // flow through this same path). Active checkbox toggles
233
+ // binding.active; mode toggle picks emit vs. assign.
234
+ // 2. List declaration row rows for each entry in an OutputList's backing array
235
+ // (mode: emit/assign, own dataType selector, own delete button)
236
+ //
237
+ // List outputs also render an "+ Add" button after their entries so new declarations
238
+ // can be appended. Errors on any row (static or list) apply a destructive ring using
239
+ // the same `outputId` key diagnostics.ts produces.
240
+
241
+ const DATA_TYPES: DataType[] = ["int", "float", "bool", "string"];
242
+
243
+ const DATA_TYPE_LABELS: Record<DataType, string> = {
244
+ int: "int",
245
+ float: "float",
246
+ bool: "bool",
247
+ string: "string",
248
+ };
249
+
250
+ /** Stable synthetic outputId used to key list-entry diagnostics (matches diagnostics.ts). */
251
+ function listEntryOutputId(listId: string, index: number): string {
252
+ return `${listId}[${index}]`;
253
+ }
254
+
255
+ /** Convert an AvailableVariable into a canonical Reference (srcId + varId). */
256
+ function availableVarToRef(
257
+ v:
258
+ | { kind: "node"; nodeId: string; outputId: string }
259
+ | { kind: "declared"; uid: string }
260
+ | { kind: "fnarg"; uid: string },
261
+ ): Reference {
262
+ if (v.kind === "node") return { srcId: v.nodeId, varId: v.outputId };
263
+ if (v.kind === "declared") return { srcId: "declared", varId: v.uid };
264
+ return { srcId: "fnarg", varId: v.uid };
265
+ }
266
+
267
+ function OutputsSection({
268
+ canvasId,
269
+ node,
270
+ nodeDefinition,
271
+ onNodeUpdate,
272
+ nodeDiags,
273
+ }: {
274
+ canvasId: string;
275
+ node: NodeData;
276
+ nodeDefinition: NodeDefinition;
277
+ onNodeUpdate: (nodeId: string, updates: { arguments?: Record<string, unknown> }) => void;
278
+ nodeDiags: Diagnostic[] | undefined;
279
+ }) {
280
+ const { t } = useTranslation();
281
+ const { list: availableVars } = useAvailableVariables(canvasId);
282
+ const availableOutput = useMemo(() => getNodeAvailableOutput(node), [node]);
283
+
284
+ const staticOutputs = useMemo(
285
+ () => (nodeDefinition.outputs ?? []).filter((o): o is StaticOutput => o.type === "static"),
286
+ [nodeDefinition],
287
+ );
288
+ const listOutputs = useMemo(
289
+ () => (nodeDefinition.outputs ?? []).filter((o): o is OutputList => o.type === "list"),
290
+ [nodeDefinition],
291
+ );
292
+
293
+ // Collect diagnostics keyed by outputId so rows can look themselves up.
294
+ const outputErrors = useMemo(() => {
295
+ const map = new Map<string, string[]>();
296
+ if (!nodeDiags) return map;
297
+ for (const d of nodeDiags) {
298
+ if (d.outputId && d.severity === "error") {
299
+ const arr = map.get(d.outputId);
300
+ if (arr) arr.push(d.message);
301
+ else map.set(d.outputId, [d.message]);
302
+ }
303
+ }
304
+ return map;
305
+ }, [nodeDiags]);
306
+
307
+ /**
308
+ * Write a binding for a static output (or FunctionCall return). List entries
309
+ * have their own update path via `replaceListEntry`.
310
+ */
311
+ const updateStaticBinding = useCallback(
312
+ (key: string, binding: OutputBinding) => {
313
+ onNodeUpdate(node.id, { arguments: { [key]: binding } });
314
+ },
315
+ [node.id, onNodeUpdate],
316
+ );
317
+
318
+ const writeListEntries = useCallback(
319
+ (listId: string, next: OutputDeclaration[]) => onNodeUpdate(node.id, { arguments: { [listId]: next } }),
320
+ [node.id, onNodeUpdate],
321
+ );
322
+
323
+ // Filter available vars to matching dataType, excluding this node's own outputs
324
+ // (a node can't assign its own output back into itself).
325
+ const filterCompatible = useCallback(
326
+ (dataType: DataType) =>
327
+ availableVars.filter((v) => v.dataType === dataType && !("nodeId" in v && v.nodeId === node.id)),
328
+ [availableVars, node.id],
329
+ );
330
+
331
+ // Any rows to show? Early-exit so the parent panel doesn't render an empty section.
332
+ if (staticOutputs.length === 0 && listOutputs.length === 0) return null;
333
+
334
+ // --- Row renderer: static output / FunctionCall return ------------------------------
335
+ // UX shape:
336
+ // ┌─ card ────────────────────────────────────────┐
337
+ // [☑] label dataType
338
+ // │ [emit | assign] [ name OR variable picker ]
339
+ // └────────────────────────────────────────────────┘
340
+ // Checkbox sits inline with the label (so the card width matches list-entry rows
341
+ // which have no checkbox). Toggles `binding.active`; mode/name/target are kept as
342
+ // draft state when inactive so off→on round-trips identically. Body is dimmed and
343
+ // pointer-event-disabled while inactive.
344
+ const renderStaticRow = (key: string, output: { name: string; dataType: DataType }, displayLabel: string) => {
345
+ const binding = getOutputBinding(node, key) ?? ({ active: true, mode: "emit", name: output.name } as OutputBinding);
346
+ const compatibleVars = filterCompatible(output.dataType);
347
+ const errors = outputErrors.get(key);
348
+ const hasError = !!errors?.length;
349
+ const enabled = binding.active;
350
+
351
+ const setEnabled = (next: boolean) => {
352
+ if (next === enabled) return;
353
+ updateStaticBinding(key, { ...binding, active: next });
354
+ };
355
+ const setMode = (mode: "emit" | "assign") => {
356
+ if (mode === binding.mode) return;
357
+ if (mode === "emit") updateStaticBinding(key, { active: binding.active, mode: "emit", name: output.name });
358
+ else updateStaticBinding(key, { active: binding.active, mode: "assign", target: { srcId: "", varId: "" } });
359
+ };
360
+
361
+ return (
362
+ <div
363
+ key={key}
364
+ className={`rounded-lg bg-card shadow-sm border p-2 space-y-2 transition-all hover:shadow-md ${hasError ? "border-destructive ring-1 ring-destructive" : "border-border"}`}
365
+ >
366
+ <div className="flex items-center gap-2">
367
+ <Checkbox
368
+ checked={enabled}
369
+ onCheckedChange={(c) => setEnabled(c === true)}
370
+ aria-label={t("outputBinding.enable", "Enable output")}
371
+ className="shrink-0"
372
+ />
373
+ <span className={`text-xs font-medium flex-1 truncate ${enabled ? "" : "opacity-50"}`}>{displayLabel}</span>
374
+ <span className={`text-xs text-muted-foreground ${enabled ? "" : "opacity-50"}`}>
375
+ {DATA_TYPE_LABELS[output.dataType]}
376
+ </span>
377
+ </div>
378
+ <div className={enabled ? "" : "opacity-50"}>
379
+ <div className={`flex items-center gap-2 ${enabled ? "" : "pointer-events-none"}`}>
380
+ <ToggleGroup
381
+ type="single"
382
+ value={binding.mode}
383
+ onValueChange={(v) => v && setMode(v as "emit" | "assign")}
384
+ className="gap-0"
385
+ >
386
+ <ToggleGroupItem
387
+ value="emit"
388
+ size="sm"
389
+ variant="outline"
390
+ className="h-7 w-7 p-0 rounded-r-none"
391
+ aria-label={t("outputBinding.emit", "Emit")}
392
+ title={t("outputBinding.emit", "Emit")}
393
+ >
394
+ <Plus className="w-3.5 h-3.5" />
395
+ </ToggleGroupItem>
396
+ <ToggleGroupItem
397
+ value="assign"
398
+ size="sm"
399
+ variant="outline"
400
+ className="h-7 w-7 p-0 rounded-l-none -ml-px"
401
+ aria-label={t("outputBinding.assign", "Assign")}
402
+ title={t("outputBinding.assign", "Assign")}
403
+ >
404
+ <ArrowRight className="w-3.5 h-3.5" />
405
+ </ToggleGroupItem>
406
+ </ToggleGroup>
407
+
408
+ {binding.mode === "emit" ? (
409
+ <Input
410
+ className="h-7 text-xs flex-1"
411
+ value={binding.name}
412
+ disabled={!enabled}
413
+ onChange={(e) =>
414
+ updateStaticBinding(key, { active: binding.active, mode: "emit", name: e.target.value })
415
+ }
416
+ />
417
+ ) : compatibleVars.length > 0 ? (
418
+ <Select
419
+ value={binding.target.srcId ? refToLookupKey(binding.target) : ""}
420
+ onValueChange={(lookupKey) => {
421
+ const v = availableVars.find((av) => varKey(av) === lookupKey);
422
+ if (!v) return;
423
+ updateStaticBinding(key, { active: binding.active, mode: "assign", target: availableVarToRef(v) });
424
+ }}
425
+ disabled={!enabled}
426
+ >
427
+ <SelectTrigger className="h-7 text-xs flex-1">
428
+ <SelectValue placeholder={t("outputBinding.selectVariable", "Select variable...")} />
429
+ </SelectTrigger>
430
+ <SelectContent>
431
+ {compatibleVars.map((v) => (
432
+ <SelectItem key={varKey(v)} value={varKey(v)}>
433
+ {v.name}
434
+ </SelectItem>
435
+ ))}
436
+ </SelectContent>
437
+ </Select>
438
+ ) : (
439
+ <span className="text-xs text-muted-foreground italic flex-1">
440
+ {t("outputBinding.noCompatibleVariables", "No compatible variables")}
441
+ </span>
442
+ )}
443
+ </div>
444
+ {hasError && (
445
+ <div className="space-y-0.5">
446
+ {errors.map((msg, i) => (
447
+ <p key={i} className="text-xs text-destructive">
448
+ {msg}
449
+ </p>
450
+ ))}
451
+ </div>
452
+ )}
453
+ </div>
454
+ </div>
455
+ );
456
+ };
457
+
458
+ // --- Row renderer: list declaration entry ------------------------------------------
459
+ const renderListEntryRow = (listId: string, entries: OutputDeclaration[], index: number) => {
460
+ const entry = entries[index];
461
+ if (!entry) return null;
462
+ const compatibleVars = filterCompatible(entry.dataType);
463
+ const outputId = listEntryOutputId(listId, index);
464
+ const errors = outputErrors.get(outputId);
465
+ const hasError = !!errors?.length;
466
+
467
+ const replace = (next: OutputDeclaration) =>
468
+ writeListEntries(
469
+ listId,
470
+ entries.map((e, i) => (i === index ? next : e)),
471
+ );
472
+ const remove = () =>
473
+ writeListEntries(
474
+ listId,
475
+ entries.filter((_, i) => i !== index),
476
+ );
477
+ // Mode flip preserves name + dataType — the user already typed/picked them.
478
+ // emit↔assign only swaps the trailing payload (uid vs target).
479
+ const changeMode = (newMode: "emit" | "assign") => {
480
+ if (entry.mode === newMode) return;
481
+ if (newMode === "emit") {
482
+ replace({ mode: "emit", uid: generateId(), name: entry.name, dataType: entry.dataType });
483
+ } else {
484
+ replace({ mode: "assign", name: entry.name, dataType: entry.dataType, target: { srcId: "", varId: "" } });
485
+ }
486
+ };
487
+ const changeName = (name: string) => replace({ ...entry, name });
488
+ const changeDataType = (dt: DataType) => replace({ ...entry, dataType: dt });
489
+
490
+ return (
491
+ <div
492
+ key={`${listId}-${entry.mode === "emit" ? entry.uid : `assign-${index}`}`}
493
+ className={`rounded-lg bg-card shadow-sm border p-2 space-y-2 transition-all hover:shadow-md ${hasError ? "border-destructive ring-1 ring-destructive" : "border-border"}`}
494
+ >
495
+ <div className="flex items-center gap-2">
496
+ {/* Row 1 mirrors OutputBinding's header: leading control (trash↔checkbox),
497
+ name (editable here, label-only there), dataType (editable here, RO there). */}
498
+ <Button variant="ghost" size="icon" className="h-7 w-7 shrink-0" onClick={remove}>
499
+ <Trash2 className="w-3.5 h-3.5" />
500
+ </Button>
501
+ <Input
502
+ className="h-7 text-xs flex-1"
503
+ value={entry.name}
504
+ placeholder={t("outputBinding.name", "Name")}
505
+ onChange={(e) => changeName(e.target.value)}
506
+ />
507
+ <Select value={entry.dataType} onValueChange={(dt: DataType) => changeDataType(dt)}>
508
+ <SelectTrigger className="h-7 text-xs w-20">
509
+ <SelectValue />
510
+ </SelectTrigger>
511
+ <SelectContent>
512
+ {DATA_TYPES.map((dt) => (
513
+ <SelectItem key={dt} value={dt}>
514
+ {dt}
515
+ </SelectItem>
516
+ ))}
517
+ </SelectContent>
518
+ </Select>
519
+ </div>
520
+ <div className="flex items-center gap-2">
521
+ <ToggleGroup
522
+ type="single"
523
+ value={entry.mode}
524
+ onValueChange={(v) => v && changeMode(v as "emit" | "assign")}
525
+ className="gap-0"
526
+ >
527
+ <ToggleGroupItem
528
+ value="emit"
529
+ size="sm"
530
+ variant="outline"
531
+ className="h-7 w-7 p-0 rounded-r-none"
532
+ aria-label={t("outputBinding.emit", "Emit")}
533
+ title={t("outputBinding.emit", "Emit")}
534
+ >
535
+ <Plus className="w-3.5 h-3.5" />
536
+ </ToggleGroupItem>
537
+ <ToggleGroupItem
538
+ value="assign"
539
+ size="sm"
540
+ variant="outline"
541
+ className="h-7 w-7 p-0 rounded-l-none -ml-px"
542
+ aria-label={t("outputBinding.assign", "Assign")}
543
+ title={t("outputBinding.assign", "Assign")}
544
+ >
545
+ <ArrowRight className="w-3.5 h-3.5" />
546
+ </ToggleGroupItem>
547
+ </ToggleGroup>
548
+ {entry.mode === "emit" ? (
549
+ <span className="text-xs text-muted-foreground italic flex-1">
550
+ {t("outputBinding.emitHint", "creates new variable in scope")}
551
+ </span>
552
+ ) : compatibleVars.length > 0 ? (
553
+ <Select
554
+ value={entry.target.srcId ? refToLookupKey(entry.target) : ""}
555
+ onValueChange={(lookupKey) => {
556
+ const v = availableVars.find((av) => varKey(av) === lookupKey);
557
+ if (!v) return;
558
+ replace({ ...entry, target: availableVarToRef(v) });
559
+ }}
560
+ >
561
+ <SelectTrigger className="h-7 text-xs flex-1">
562
+ <SelectValue placeholder={t("outputBinding.selectVariable", "Select variable...")} />
563
+ </SelectTrigger>
564
+ <SelectContent>
565
+ {compatibleVars.map((v) => (
566
+ <SelectItem key={varKey(v)} value={varKey(v)}>
567
+ {v.name}
568
+ </SelectItem>
569
+ ))}
570
+ </SelectContent>
571
+ </Select>
572
+ ) : (
573
+ <span className="text-xs text-muted-foreground italic flex-1">
574
+ {t("outputBinding.noCompatibleVariables", "No compatible variables")}
575
+ </span>
576
+ )}
577
+ </div>
578
+ {hasError && (
579
+ <div className="space-y-0.5">
580
+ {errors.map((msg, i) => (
581
+ <p key={i} className="text-xs text-destructive">
582
+ {msg}
583
+ </p>
584
+ ))}
585
+ </div>
586
+ )}
587
+ </div>
588
+ );
589
+ };
590
+
591
+ const addListEntry = (listId: string, entries: OutputDeclaration[]) => {
592
+ const fresh: OutputDeclaration = {
593
+ mode: "emit",
594
+ uid: generateId(),
595
+ name: `output${entries.length + 1}`,
596
+ dataType: "string",
597
+ };
598
+ writeListEntries(listId, [...entries, fresh]);
599
+ };
600
+
601
+ return (
602
+ <div className="space-y-2">
603
+ <p className="text-xs font-medium text-muted-foreground uppercase tracking-wide">{t("outputs", "Outputs")}</p>
604
+ <div className="space-y-2">
605
+ {/* Static outputs from the node definition (FunctionCall returns flow here too). */}
606
+ {staticOutputs.map((out) => {
607
+ const output = availableOutput[out.id];
608
+ if (!output) return null;
609
+ return renderStaticRow(out.id, output, out.label);
610
+ })}
611
+
612
+ {/* List outputs — each list gets a minor subheader (its label), then its entries,
613
+ then an "Add" button. The subheader also serves as the visual boundary between
614
+ static outputs above and list entries below. */}
615
+ {listOutputs.map((out) => {
616
+ const entries =
617
+ ((node.arguments as Record<string, unknown>)[out.id] as OutputDeclaration[] | undefined) ?? [];
618
+ return (
619
+ <Fragment key={out.id}>
620
+ <div className="flex items-center gap-2 pt-1">
621
+ <span className="text-[11px] font-medium text-muted-foreground">{out.label}</span>
622
+ <div className="flex-1 h-px bg-border/60" />
623
+ </div>
624
+ {entries.map((_, index) => renderListEntryRow(out.id, entries, index))}
625
+ <AddButton onClick={() => addListEntry(out.id, entries)}>
626
+ {t("addOutput", { label: out.label, defaultValue: `Add ${out.label.toLowerCase()}` })}
627
+ </AddButton>
628
+ </Fragment>
629
+ );
630
+ })}
631
+ </div>
632
+ </div>
633
+ );
634
+ }