@nyaruka/temba-components 0.129.9 → 0.129.11

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 (323) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/demo/data/flows/sample-flow.json +96 -56
  3. package/demo/test-colorpicker.html +30 -0
  4. package/dist/temba-components.js +896 -934
  5. package/dist/temba-components.js.map +1 -1
  6. package/out-tsc/src/events.js.map +1 -1
  7. package/out-tsc/src/flow/Editor.js +9 -6
  8. package/out-tsc/src/flow/Editor.js.map +1 -1
  9. package/out-tsc/src/flow/actions/set_contact_channel.js +1 -1
  10. package/out-tsc/src/flow/actions/set_contact_channel.js.map +1 -1
  11. package/out-tsc/src/flow/actions/set_contact_field.js +1 -1
  12. package/out-tsc/src/flow/actions/set_contact_field.js.map +1 -1
  13. package/out-tsc/src/flow/actions/set_contact_language.js +1 -1
  14. package/out-tsc/src/flow/actions/set_contact_language.js.map +1 -1
  15. package/out-tsc/src/flow/actions/set_contact_status.js +1 -1
  16. package/out-tsc/src/flow/actions/set_contact_status.js.map +1 -1
  17. package/out-tsc/src/flow/config.js +2 -8
  18. package/out-tsc/src/flow/config.js.map +1 -1
  19. package/out-tsc/src/flow/nodes/split_by_llm.js +101 -0
  20. package/out-tsc/src/flow/nodes/split_by_llm.js.map +1 -0
  21. package/out-tsc/src/flow/nodes/split_by_llm_categorize.js +4 -89
  22. package/out-tsc/src/flow/nodes/split_by_llm_categorize.js.map +1 -1
  23. package/out-tsc/src/flow/nodes/split_by_subflow.js +123 -3
  24. package/out-tsc/src/flow/nodes/split_by_subflow.js.map +1 -1
  25. package/out-tsc/src/flow/nodes/split_by_ticket.js +115 -13
  26. package/out-tsc/src/flow/nodes/split_by_ticket.js.map +1 -1
  27. package/out-tsc/src/flow/nodes/split_by_webhook.js +160 -12
  28. package/out-tsc/src/flow/nodes/split_by_webhook.js.map +1 -1
  29. package/out-tsc/src/form/ArrayEditor.js +45 -56
  30. package/out-tsc/src/form/ArrayEditor.js.map +1 -1
  31. package/out-tsc/src/form/BaseListEditor.js +4 -3
  32. package/out-tsc/src/form/BaseListEditor.js.map +1 -1
  33. package/out-tsc/src/form/Checkbox.js +77 -24
  34. package/out-tsc/src/form/Checkbox.js.map +1 -1
  35. package/out-tsc/src/form/ColorPicker.js +28 -40
  36. package/out-tsc/src/form/ColorPicker.js.map +1 -1
  37. package/out-tsc/src/form/Completion.js +44 -53
  38. package/out-tsc/src/form/Completion.js.map +1 -1
  39. package/out-tsc/src/form/Compose.js +7 -8
  40. package/out-tsc/src/form/Compose.js.map +1 -1
  41. package/out-tsc/src/form/ContactSearch.js +3 -4
  42. package/out-tsc/src/form/ContactSearch.js.map +1 -1
  43. package/out-tsc/src/form/DatePicker.js +29 -36
  44. package/out-tsc/src/form/DatePicker.js.map +1 -1
  45. package/out-tsc/src/form/{FormField.js → FieldElement.js} +78 -50
  46. package/out-tsc/src/form/FieldElement.js.map +1 -0
  47. package/out-tsc/src/form/FieldRenderer.js +2 -1
  48. package/out-tsc/src/form/FieldRenderer.js.map +1 -1
  49. package/out-tsc/src/form/ImagePicker.js +122 -126
  50. package/out-tsc/src/form/ImagePicker.js.map +1 -1
  51. package/out-tsc/src/form/KeyValueEditor.js +41 -37
  52. package/out-tsc/src/form/KeyValueEditor.js.map +1 -1
  53. package/out-tsc/src/form/MessageEditor.js +55 -63
  54. package/out-tsc/src/form/MessageEditor.js.map +1 -1
  55. package/out-tsc/src/form/TembaSlider.js +3 -3
  56. package/out-tsc/src/form/TembaSlider.js.map +1 -1
  57. package/out-tsc/src/form/TemplateEditor.js +3 -3
  58. package/out-tsc/src/form/TemplateEditor.js.map +1 -1
  59. package/out-tsc/src/form/TextInput.js +22 -26
  60. package/out-tsc/src/form/TextInput.js.map +1 -1
  61. package/out-tsc/src/form/select/Select.js +9 -15
  62. package/out-tsc/src/form/select/Select.js.map +1 -1
  63. package/out-tsc/src/form/select/UserSelect.js +8 -9
  64. package/out-tsc/src/form/select/UserSelect.js.map +1 -1
  65. package/out-tsc/src/form/select/WorkspaceSelect.js +7 -8
  66. package/out-tsc/src/form/select/WorkspaceSelect.js.map +1 -1
  67. package/out-tsc/src/live/ContactChat.js +73 -99
  68. package/out-tsc/src/live/ContactChat.js.map +1 -1
  69. package/out-tsc/src/live/ContactFieldEditor.js.map +1 -1
  70. package/out-tsc/src/utils.js +115 -0
  71. package/out-tsc/src/utils.js.map +1 -1
  72. package/out-tsc/temba-modules.js +3 -2
  73. package/out-tsc/temba-modules.js.map +1 -1
  74. package/out-tsc/test/nodes/split_by_llm.test.js +174 -0
  75. package/out-tsc/test/nodes/split_by_llm.test.js.map +1 -0
  76. package/out-tsc/test/temba-checkbox.test.js +16 -0
  77. package/out-tsc/test/temba-checkbox.test.js.map +1 -1
  78. package/out-tsc/test/temba-integration-markdown.test.js +2 -4
  79. package/out-tsc/test/temba-integration-markdown.test.js.map +1 -1
  80. package/out-tsc/test/temba-slider.test.js +0 -1
  81. package/out-tsc/test/temba-slider.test.js.map +1 -1
  82. package/package.json +1 -1
  83. package/screenshots/truth/actions/add_contact_groups/editor/descriptive-group-names.png +0 -0
  84. package/screenshots/truth/actions/add_contact_groups/editor/long-group-names.png +0 -0
  85. package/screenshots/truth/actions/add_contact_groups/editor/many-groups.png +0 -0
  86. package/screenshots/truth/actions/add_contact_groups/editor/multiple-groups.png +0 -0
  87. package/screenshots/truth/actions/add_contact_groups/editor/single-group.png +0 -0
  88. package/screenshots/truth/actions/call_llm/editor/information-extraction.png +0 -0
  89. package/screenshots/truth/actions/call_llm/editor/sentiment-analysis.png +0 -0
  90. package/screenshots/truth/actions/call_llm/editor/summarization.png +0 -0
  91. package/screenshots/truth/actions/call_llm/editor/translation-task.png +0 -0
  92. package/screenshots/truth/actions/remove_contact_groups/editor/cleanup-groups.png +0 -0
  93. package/screenshots/truth/actions/remove_contact_groups/editor/long-descriptive-group-names.png +0 -0
  94. package/screenshots/truth/actions/remove_contact_groups/editor/many-groups.png +0 -0
  95. package/screenshots/truth/actions/remove_contact_groups/editor/multiple-groups.png +0 -0
  96. package/screenshots/truth/actions/remove_contact_groups/editor/remove-from-all-groups.png +0 -0
  97. package/screenshots/truth/actions/remove_contact_groups/editor/single-group.png +0 -0
  98. package/screenshots/truth/actions/send_email/editor/complex-business-email.png +0 -0
  99. package/screenshots/truth/actions/send_email/editor/empty-body.png +0 -0
  100. package/screenshots/truth/actions/send_email/editor/empty-subject.png +0 -0
  101. package/screenshots/truth/actions/send_email/editor/long-subject.png +0 -0
  102. package/screenshots/truth/actions/send_email/editor/multiline-body.png +0 -0
  103. package/screenshots/truth/actions/send_email/editor/multiple-recipients.png +0 -0
  104. package/screenshots/truth/actions/send_email/editor/simple-email.png +0 -0
  105. package/screenshots/truth/actions/send_email/editor/with-expressions.png +0 -0
  106. package/screenshots/truth/actions/send_msg/editor/long-quick-replies.png +0 -0
  107. package/screenshots/truth/actions/send_msg/editor/multiline-text-with-replies.png +0 -0
  108. package/screenshots/truth/actions/send_msg/editor/simple-text.png +0 -0
  109. package/screenshots/truth/actions/send_msg/editor/text-with-linebreaks.png +0 -0
  110. package/screenshots/truth/actions/send_msg/editor/text-with-many-quick-replies.png +0 -0
  111. package/screenshots/truth/actions/send_msg/editor/text-with-quick-replies.png +0 -0
  112. package/screenshots/truth/actions/send_msg/editor/text-without-quick-replies.png +0 -0
  113. package/screenshots/truth/checkbox/checkbox-label-background-hover.png +0 -0
  114. package/screenshots/truth/checkbox/checkbox-no-label-no-background-hover.png +0 -0
  115. package/screenshots/truth/checkbox/checkbox-whitespace-label-no-background-hover.png +0 -0
  116. package/screenshots/truth/checkbox/checkbox-with-help-text.png +0 -0
  117. package/screenshots/truth/checkbox/checked.png +0 -0
  118. package/screenshots/truth/checkbox/default.png +0 -0
  119. package/screenshots/truth/colorpicker/default.png +0 -0
  120. package/screenshots/truth/colorpicker/focused.png +0 -0
  121. package/screenshots/truth/colorpicker/initialized.png +0 -0
  122. package/screenshots/truth/colorpicker/selected.png +0 -0
  123. package/screenshots/truth/compose/attachments-tab.png +0 -0
  124. package/screenshots/truth/compose/attachments-with-files.png +0 -0
  125. package/screenshots/truth/compose/intial-text.png +0 -0
  126. package/screenshots/truth/compose/no-counter.png +0 -0
  127. package/screenshots/truth/compose/wraps-text-and-spaces.png +0 -0
  128. package/screenshots/truth/compose/wraps-text-and-url.png +0 -0
  129. package/screenshots/truth/compose/wraps-text-no-spaces.png +0 -0
  130. package/screenshots/truth/contacts/chat-failure.png +0 -0
  131. package/screenshots/truth/contacts/chat-for-active-contact.png +0 -0
  132. package/screenshots/truth/contacts/chat-sends-attachments-only.png +0 -0
  133. package/screenshots/truth/contacts/chat-sends-text-and-attachments.png +0 -0
  134. package/screenshots/truth/contacts/chat-sends-text-only.png +0 -0
  135. package/screenshots/truth/counter/summary.png +0 -0
  136. package/screenshots/truth/counter/text.png +0 -0
  137. package/screenshots/truth/counter/unicode-variables.png +0 -0
  138. package/screenshots/truth/counter/unicode.png +0 -0
  139. package/screenshots/truth/counter/variable.png +0 -0
  140. package/screenshots/truth/datepicker/date-truncated-time.png +0 -0
  141. package/screenshots/truth/datepicker/date.png +0 -0
  142. package/screenshots/truth/datepicker/initial-timezone.png +0 -0
  143. package/screenshots/truth/datepicker/range-picker-editing-start.png +0 -0
  144. package/screenshots/truth/datepicker/updated-keyboard-date.png +0 -0
  145. package/screenshots/truth/dialog/focused.png +0 -0
  146. package/screenshots/truth/dropdown/right-edge-collision.png +0 -0
  147. package/screenshots/truth/editor/router.png +0 -0
  148. package/screenshots/truth/editor/send_msg.png +0 -0
  149. package/screenshots/truth/editor/set_contact_language.png +0 -0
  150. package/screenshots/truth/editor/set_contact_name.png +0 -0
  151. package/screenshots/truth/editor/set_run_result.png +0 -0
  152. package/screenshots/truth/editor/wait.png +0 -0
  153. package/screenshots/truth/field-renderer/checkbox-checked.png +0 -0
  154. package/screenshots/truth/field-renderer/checkbox-unchecked.png +0 -0
  155. package/screenshots/truth/field-renderer/checkbox-with-errors.png +0 -0
  156. package/screenshots/truth/field-renderer/context-comparison.png +0 -0
  157. package/screenshots/truth/field-renderer/key-value-with-label.png +0 -0
  158. package/screenshots/truth/field-renderer/message-editor-with-label.png +0 -0
  159. package/screenshots/truth/field-renderer/select-multi.png +0 -0
  160. package/screenshots/truth/field-renderer/select-no-label.png +0 -0
  161. package/screenshots/truth/field-renderer/select-with-label.png +0 -0
  162. package/screenshots/truth/field-renderer/text-evaluated.png +0 -0
  163. package/screenshots/truth/field-renderer/text-no-label.png +0 -0
  164. package/screenshots/truth/field-renderer/text-with-errors.png +0 -0
  165. package/screenshots/truth/field-renderer/text-with-label.png +0 -0
  166. package/screenshots/truth/field-renderer/textarea-evaluated.png +0 -0
  167. package/screenshots/truth/field-renderer/textarea-with-label.png +0 -0
  168. package/screenshots/truth/integration/checkbox-markdown-errors.png +0 -0
  169. package/screenshots/truth/list/fields-dragging.png +0 -0
  170. package/screenshots/truth/list/fields-filtered.png +0 -0
  171. package/screenshots/truth/list/fields-hovered.png +0 -0
  172. package/screenshots/truth/list/fields.png +0 -0
  173. package/screenshots/truth/list/items-selected.png +0 -0
  174. package/screenshots/truth/list/items-updated.png +0 -0
  175. package/screenshots/truth/list/items.png +0 -0
  176. package/screenshots/truth/menu/menu-focused-with items.png +0 -0
  177. package/screenshots/truth/menu/menu-refresh-1.png +0 -0
  178. package/screenshots/truth/menu/menu-refresh-2.png +0 -0
  179. package/screenshots/truth/menu/menu-submenu.png +0 -0
  180. package/screenshots/truth/menu/menu-tasks-nextup.png +0 -0
  181. package/screenshots/truth/menu/menu-tasks.png +0 -0
  182. package/screenshots/truth/message-editor/autogrow-initial-content.png +0 -0
  183. package/screenshots/truth/message-editor/default.png +0 -0
  184. package/screenshots/truth/message-editor/drag-highlight.png +0 -0
  185. package/screenshots/truth/message-editor/filtered-attachments.png +0 -0
  186. package/screenshots/truth/message-editor/with-completion.png +0 -0
  187. package/screenshots/truth/message-editor/with-properties.png +0 -0
  188. package/screenshots/truth/modax/form.png +0 -0
  189. package/screenshots/truth/modax/simple.png +0 -0
  190. package/screenshots/truth/nodes/split_by_llm/editor/information-extraction.png +0 -0
  191. package/screenshots/truth/nodes/split_by_llm/editor/sentiment-analysis.png +0 -0
  192. package/screenshots/truth/nodes/split_by_llm/editor/summarization.png +0 -0
  193. package/screenshots/truth/nodes/split_by_llm/editor/translation-task.png +0 -0
  194. package/screenshots/truth/nodes/split_by_llm/render/information-extraction.png +0 -0
  195. package/screenshots/truth/nodes/split_by_llm/render/sentiment-analysis.png +0 -0
  196. package/screenshots/truth/nodes/split_by_llm/render/summarization.png +0 -0
  197. package/screenshots/truth/nodes/split_by_llm/render/translation-task.png +0 -0
  198. package/screenshots/truth/nodes/split_by_llm_categorize/editor/basic-categorization.png +0 -0
  199. package/screenshots/truth/nodes/split_by_llm_categorize/editor/custom-input-and-result-name.png +0 -0
  200. package/screenshots/truth/nodes/split_by_llm_categorize/editor/feedback-categorization.png +0 -0
  201. package/screenshots/truth/nodes/split_by_llm_categorize/editor/many-categories.png +0 -0
  202. package/screenshots/truth/nodes/split_by_llm_categorize/editor/minimal-categories.png +0 -0
  203. package/screenshots/truth/nodes/split_by_random/editor/ab-test-multiple-variants.png +0 -0
  204. package/screenshots/truth/nodes/split_by_random/editor/sampling-split.png +0 -0
  205. package/screenshots/truth/nodes/split_by_random/editor/three-way-split.png +0 -0
  206. package/screenshots/truth/nodes/split_by_random/editor/two-bucket-split.png +0 -0
  207. package/screenshots/truth/nodes/wait_for_digits/editor/basic-digits-wait.png +0 -0
  208. package/screenshots/truth/nodes/wait_for_digits/editor/phone-number-collection.png +0 -0
  209. package/screenshots/truth/nodes/wait_for_digits/editor/single-digit-with-timeout.png +0 -0
  210. package/screenshots/truth/nodes/wait_for_digits/editor/verification-code.png +0 -0
  211. package/screenshots/truth/nodes/wait_for_response/editor/basic-wait.png +0 -0
  212. package/screenshots/truth/nodes/wait_for_response/editor/custom-result-name.png +0 -0
  213. package/screenshots/truth/nodes/wait_for_response/editor/no-timeout.png +0 -0
  214. package/screenshots/truth/nodes/wait_for_response/editor/short-timeout.png +0 -0
  215. package/screenshots/truth/omnibox/selected.png +0 -0
  216. package/screenshots/truth/options/block.png +0 -0
  217. package/screenshots/truth/run-list/basic.png +0 -0
  218. package/screenshots/truth/select/disabled-multi-selection.png +0 -0
  219. package/screenshots/truth/select/disabled-selection.png +0 -0
  220. package/screenshots/truth/select/disabled.png +0 -0
  221. package/screenshots/truth/select/embedded.png +0 -0
  222. package/screenshots/truth/select/empty-options.png +0 -0
  223. package/screenshots/truth/select/expression-selected.png +0 -0
  224. package/screenshots/truth/select/expressions.png +0 -0
  225. package/screenshots/truth/select/functions.png +0 -0
  226. package/screenshots/truth/select/local-options.png +0 -0
  227. package/screenshots/truth/select/multi-with-endpoint.png +0 -0
  228. package/screenshots/truth/select/multiple-initial-values.png +0 -0
  229. package/screenshots/truth/select/remote-options.png +0 -0
  230. package/screenshots/truth/select/search-enabled.png +0 -0
  231. package/screenshots/truth/select/search-multi-no-matches.png +0 -0
  232. package/screenshots/truth/select/search-selected-focus.png +0 -0
  233. package/screenshots/truth/select/search-selected.png +0 -0
  234. package/screenshots/truth/select/search-with-selected.png +0 -0
  235. package/screenshots/truth/select/searching.png +0 -0
  236. package/screenshots/truth/select/selected-multi-maxitems-reached.png +0 -0
  237. package/screenshots/truth/select/selected-multi.png +0 -0
  238. package/screenshots/truth/select/selected-single.png +0 -0
  239. package/screenshots/truth/select/selection-clearable.png +0 -0
  240. package/screenshots/truth/select/static-initial-value.png +0 -0
  241. package/screenshots/truth/select/static-initial-via-selected.png +0 -0
  242. package/screenshots/truth/select/truncated-selection.png +0 -0
  243. package/screenshots/truth/select/with-placeholder.png +0 -0
  244. package/screenshots/truth/select/without-placeholder.png +0 -0
  245. package/screenshots/truth/slider/update-slider-on-circle-dragged.png +0 -0
  246. package/screenshots/truth/templates/default.png +0 -0
  247. package/screenshots/truth/templates/unapproved.png +0 -0
  248. package/screenshots/truth/textinput/autogrow-initial.png +0 -0
  249. package/screenshots/truth/textinput/input-disabled.png +0 -0
  250. package/screenshots/truth/textinput/input-focused.png +0 -0
  251. package/screenshots/truth/textinput/input-form.png +0 -0
  252. package/screenshots/truth/textinput/input-inserted.png +0 -0
  253. package/screenshots/truth/textinput/input-placeholder.png +0 -0
  254. package/screenshots/truth/textinput/input-updated.png +0 -0
  255. package/screenshots/truth/textinput/input.png +0 -0
  256. package/screenshots/truth/textinput/textarea-focused.png +0 -0
  257. package/screenshots/truth/textinput/textarea.png +0 -0
  258. package/src/events.ts +9 -8
  259. package/src/flow/Editor.ts +6 -3
  260. package/src/flow/actions/set_contact_channel.ts +1 -1
  261. package/src/flow/actions/set_contact_field.ts +1 -1
  262. package/src/flow/actions/set_contact_language.ts +1 -1
  263. package/src/flow/actions/set_contact_status.ts +1 -1
  264. package/src/flow/config.ts +2 -8
  265. package/src/flow/nodes/split_by_llm.ts +119 -0
  266. package/src/flow/nodes/split_by_llm_categorize.ts +13 -116
  267. package/src/flow/nodes/split_by_subflow.ts +153 -3
  268. package/src/flow/nodes/split_by_ticket.ts +135 -12
  269. package/src/flow/nodes/split_by_webhook.ts +187 -12
  270. package/src/form/ArrayEditor.ts +45 -57
  271. package/src/form/BaseListEditor.ts +4 -4
  272. package/src/form/Checkbox.ts +81 -24
  273. package/src/form/ColorPicker.ts +31 -43
  274. package/src/form/Completion.ts +49 -56
  275. package/src/form/Compose.ts +8 -8
  276. package/src/form/ContactSearch.ts +3 -4
  277. package/src/form/DatePicker.ts +32 -38
  278. package/src/form/{FormField.ts → FieldElement.ts} +105 -52
  279. package/src/form/FieldRenderer.ts +2 -1
  280. package/src/form/ImagePicker.ts +107 -110
  281. package/src/form/KeyValueEditor.ts +43 -39
  282. package/src/form/MessageEditor.ts +61 -67
  283. package/src/form/TembaSlider.ts +3 -3
  284. package/src/form/TemplateEditor.ts +3 -3
  285. package/src/form/TextInput.ts +25 -28
  286. package/src/form/select/Select.ts +12 -17
  287. package/src/form/select/UserSelect.ts +10 -11
  288. package/src/form/select/WorkspaceSelect.ts +9 -10
  289. package/src/live/ContactChat.ts +81 -92
  290. package/src/live/ContactFieldEditor.ts +2 -2
  291. package/src/store/flow-definition.d.ts +2 -1
  292. package/src/utils.ts +192 -0
  293. package/temba-modules.ts +3 -2
  294. package/test/nodes/split_by_llm.test.ts +232 -0
  295. package/test/temba-checkbox.test.ts +26 -0
  296. package/test/temba-integration-markdown.test.ts +2 -4
  297. package/test/temba-slider.test.ts +0 -1
  298. package/test-assets/contacts/history.json +7 -20
  299. package/test-assets/style.css +36 -234
  300. package/web-dev-server.config.mjs +26 -0
  301. package/web-test-runner.config.mjs +1 -1
  302. package/out-tsc/src/flow/actions/call_llm.js +0 -64
  303. package/out-tsc/src/flow/actions/call_llm.js.map +0 -1
  304. package/out-tsc/src/flow/actions/call_webhook.js +0 -131
  305. package/out-tsc/src/flow/actions/call_webhook.js.map +0 -1
  306. package/out-tsc/src/flow/actions/enter_flow.js +0 -14
  307. package/out-tsc/src/flow/actions/enter_flow.js.map +0 -1
  308. package/out-tsc/src/flow/actions/open_ticket.js +0 -73
  309. package/out-tsc/src/flow/actions/open_ticket.js.map +0 -1
  310. package/out-tsc/src/form/FormElement.js +0 -67
  311. package/out-tsc/src/form/FormElement.js.map +0 -1
  312. package/out-tsc/src/form/FormField.js.map +0 -1
  313. package/out-tsc/test/actions/call_llm.test.js +0 -103
  314. package/out-tsc/test/actions/call_llm.test.js.map +0 -1
  315. package/out-tsc/test/temba-formfield.test.js +0 -94
  316. package/out-tsc/test/temba-formfield.test.js.map +0 -1
  317. package/src/flow/actions/call_llm.ts +0 -66
  318. package/src/flow/actions/call_webhook.ts +0 -143
  319. package/src/flow/actions/enter_flow.ts +0 -15
  320. package/src/flow/actions/open_ticket.ts +0 -83
  321. package/src/form/FormElement.ts +0 -69
  322. package/test/actions/call_llm.test.ts +0 -137
  323. package/test/temba-formfield.test.ts +0 -121
@@ -1,6 +1,6 @@
1
1
  import { COLORS, NodeConfig } from '../types';
2
- import { Node } from '../../store/flow-definition';
3
- import { generateUUID } from '../../utils';
2
+ import { CallLLM, Node } from '../../store/flow-definition';
3
+ import { generateUUID, createMultiCategoryRouter } from '../../utils';
4
4
  import { html } from 'lit';
5
5
 
6
6
  export const split_by_llm_categorize: NodeConfig = {
@@ -92,7 +92,7 @@ export const split_by_llm_categorize: NodeConfig = {
92
92
  render: (node: Node) => {
93
93
  const callLlmAction = node.actions?.find(
94
94
  (action) => action.type === 'call_llm'
95
- ) as any;
95
+ ) as CallLLM;
96
96
  return html`
97
97
  <div class="body">Categorize with ${callLlmAction.llm.name}</div>
98
98
  `;
@@ -135,7 +135,7 @@ export const split_by_llm_categorize: NodeConfig = {
135
135
  const callLlmUuid = existingCallLlmAction?.uuid || generateUUID();
136
136
 
137
137
  // Create call_llm action (using any type to match the example format)
138
- const callLlmAction: any = {
138
+ const callLlmAction: CallLLM = {
139
139
  type: 'call_llm',
140
140
  uuid: callLlmUuid,
141
141
  llm: llmSelection
@@ -147,124 +147,21 @@ export const split_by_llm_categorize: NodeConfig = {
147
147
  };
148
148
 
149
149
  // Create categories and exits
150
- const categories = [];
151
- const exits = [];
152
- const cases = [];
153
-
154
- // Get existing categories from original node for UUID preservation
155
150
  const existingCategories = originalNode.router?.categories || [];
156
151
  const existingExits = originalNode.exits || [];
157
152
  const existingCases = originalNode.router?.cases || [];
158
153
 
159
- // Add user categories
160
- userCategories.forEach((categoryName: string) => {
161
- // Check if this category already exists
162
- const existingCategory = existingCategories.find(
163
- (cat) => cat.name === categoryName
164
- );
165
- const existingExit = existingCategory
166
- ? existingExits.find((exit) => exit.uuid === existingCategory.exit_uuid)
167
- : null;
168
- const existingCase = existingCategory
169
- ? existingCases.find(
170
- (case_) => case_.category_uuid === existingCategory.uuid
171
- )
172
- : null;
173
-
174
- // Use existing UUIDs if category name hasn't changed, otherwise generate new ones
175
- const categoryUuid = existingCategory?.uuid || generateUUID();
176
- const exitUuid = existingExit?.uuid || generateUUID();
177
- const caseUuid = existingCase?.uuid || generateUUID();
178
-
179
- categories.push({
180
- uuid: categoryUuid,
181
- name: categoryName,
182
- exit_uuid: exitUuid
183
- });
184
-
185
- exits.push({
186
- uuid: exitUuid,
187
- destination_uuid: existingExit?.destination_uuid || null
188
- });
189
-
190
- cases.push({
191
- uuid: caseUuid,
154
+ const { router, exits } = createMultiCategoryRouter(
155
+ '@locals._llm_output',
156
+ userCategories,
157
+ (categoryName) => ({
192
158
  type: 'has_only_text',
193
- arguments: [categoryName],
194
- category_uuid: categoryUuid
195
- });
196
- });
197
-
198
- // Add "Other" category (default)
199
- const existingOtherCategory = existingCategories.find(
200
- (cat) => cat.name === 'Other'
201
- );
202
- const existingOtherExit = existingOtherCategory
203
- ? existingExits.find(
204
- (exit) => exit.uuid === existingOtherCategory.exit_uuid
205
- )
206
- : null;
207
-
208
- const otherCategoryUuid = existingOtherCategory?.uuid || generateUUID();
209
- const otherExitUuid = existingOtherExit?.uuid || generateUUID();
210
-
211
- categories.push({
212
- uuid: otherCategoryUuid,
213
- name: 'Other',
214
- exit_uuid: otherExitUuid
215
- });
216
- exits.push({
217
- uuid: otherExitUuid,
218
- destination_uuid: existingOtherExit?.destination_uuid || null
219
- });
220
-
221
- // Add "Failure" category
222
- const existingFailureCategory = existingCategories.find(
223
- (cat) => cat.name === 'Failure'
159
+ arguments: [categoryName]
160
+ }),
161
+ existingCategories,
162
+ existingExits,
163
+ existingCases
224
164
  );
225
- const existingFailureExit = existingFailureCategory
226
- ? existingExits.find(
227
- (exit) => exit.uuid === existingFailureCategory.exit_uuid
228
- )
229
- : null;
230
- const existingFailureCase = existingFailureCategory
231
- ? existingCases.find(
232
- (case_) =>
233
- case_.category_uuid === existingFailureCategory.uuid &&
234
- case_.arguments?.[0] === '<ERROR>'
235
- )
236
- : null;
237
-
238
- const failureCategoryUuid = existingFailureCategory?.uuid || generateUUID();
239
- const failureExitUuid = existingFailureExit?.uuid || generateUUID();
240
- const failureCaseUuid = existingFailureCase?.uuid || generateUUID();
241
-
242
- categories.push({
243
- uuid: failureCategoryUuid,
244
- name: 'Failure',
245
- exit_uuid: failureExitUuid
246
- });
247
- exits.push({
248
- uuid: failureExitUuid,
249
- destination_uuid: existingFailureExit?.destination_uuid || null
250
- });
251
-
252
- // Add failure case for <ERROR>
253
- cases.push({
254
- uuid: failureCaseUuid,
255
- type: 'has_only_text',
256
- arguments: ['<ERROR>'],
257
- category_uuid: failureCategoryUuid
258
- });
259
-
260
- // Create the router
261
- const router = {
262
- type: 'switch' as const,
263
- categories: categories,
264
- default_category_uuid: otherCategoryUuid,
265
- operand: '@locals._llm_output',
266
- cases: cases
267
- };
268
165
 
269
166
  // Return the complete node
270
167
  return {
@@ -1,9 +1,159 @@
1
- import { enter_flow } from '../actions/enter_flow';
2
1
  import { COLORS, NodeConfig } from '../types';
2
+ import { Node } from '../../store/flow-definition';
3
+ import { generateUUID } from '../../utils';
4
+ import { html } from 'lit';
3
5
 
4
6
  export const split_by_subflow: NodeConfig = {
5
7
  type: 'split_by_subflow',
6
- name: 'Split by Subflow',
8
+ name: 'Enter a Flow',
7
9
  color: COLORS.execute,
8
- action: enter_flow
10
+ form: {
11
+ flow: {
12
+ type: 'select',
13
+ required: true,
14
+ placeholder: 'Select a flow...',
15
+ helpText:
16
+ 'Once the subflow is complete or expires, the contact will return here',
17
+ endpoint: '/api/v2/flows.json',
18
+ valueKey: 'uuid',
19
+ nameKey: 'name'
20
+ }
21
+ },
22
+ layout: ['flow'],
23
+ render: (node: Node) => {
24
+ const enterFlowAction = node.actions?.find(
25
+ (action) => action.type === 'enter_flow'
26
+ ) as any;
27
+ return html`
28
+ <div class="body">
29
+ ${enterFlowAction?.flow?.name || 'Configure subflow'}
30
+ </div>
31
+ `;
32
+ },
33
+ toFormData: (node: Node) => {
34
+ // Extract data from the existing node structure
35
+ const enterFlowAction = node.actions?.find(
36
+ (action) => action.type === 'enter_flow'
37
+ ) as any;
38
+
39
+ return {
40
+ uuid: node.uuid,
41
+ flow: enterFlowAction?.flow
42
+ ? [{ uuid: enterFlowAction.flow.uuid, name: enterFlowAction.flow.name }]
43
+ : []
44
+ };
45
+ },
46
+ fromFormData: (formData: any, originalNode: Node): Node => {
47
+ // Get flow selection
48
+ const flowSelection =
49
+ Array.isArray(formData.flow) && formData.flow.length > 0
50
+ ? formData.flow[0]
51
+ : null;
52
+
53
+ // Find existing enter_flow action to preserve its UUID
54
+ const existingEnterFlowAction = originalNode.actions?.find(
55
+ (action) => action.type === 'enter_flow'
56
+ );
57
+ const enterFlowUuid = existingEnterFlowAction?.uuid || generateUUID();
58
+
59
+ // Create enter_flow action
60
+ const enterFlowAction: any = {
61
+ type: 'enter_flow',
62
+ uuid: enterFlowUuid,
63
+ flow: flowSelection
64
+ ? {
65
+ uuid: flowSelection.uuid || flowSelection.value,
66
+ name: flowSelection.name
67
+ }
68
+ : { uuid: '', name: '' }
69
+ };
70
+
71
+ // Create categories and exits for Complete and Expired
72
+ const existingCategories = originalNode.router?.categories || [];
73
+ const existingExits = originalNode.exits || [];
74
+ const existingCases = originalNode.router?.cases || [];
75
+
76
+ // Find existing Complete category
77
+ const existingCompleteCategory = existingCategories.find(
78
+ (cat) => cat.name === 'Complete'
79
+ );
80
+ const existingCompleteExit = existingCompleteCategory
81
+ ? existingExits.find(
82
+ (exit) => exit.uuid === existingCompleteCategory.exit_uuid
83
+ )
84
+ : null;
85
+ const existingCompleteCase = existingCompleteCategory
86
+ ? existingCases.find(
87
+ (case_) => case_.category_uuid === existingCompleteCategory.uuid
88
+ )
89
+ : null;
90
+
91
+ const completeCategoryUuid =
92
+ existingCompleteCategory?.uuid || generateUUID();
93
+ const completeExitUuid = existingCompleteExit?.uuid || generateUUID();
94
+ const completeCaseUuid = existingCompleteCase?.uuid || generateUUID();
95
+
96
+ // Find existing Expired category
97
+ const existingExpiredCategory = existingCategories.find(
98
+ (cat) => cat.name === 'Expired'
99
+ );
100
+ const existingExpiredExit = existingExpiredCategory
101
+ ? existingExits.find(
102
+ (exit) => exit.uuid === existingExpiredCategory.exit_uuid
103
+ )
104
+ : null;
105
+
106
+ const expiredCategoryUuid = existingExpiredCategory?.uuid || generateUUID();
107
+ const expiredExitUuid = existingExpiredExit?.uuid || generateUUID();
108
+
109
+ const categories = [
110
+ {
111
+ uuid: completeCategoryUuid,
112
+ name: 'Complete',
113
+ exit_uuid: completeExitUuid
114
+ },
115
+ {
116
+ uuid: expiredCategoryUuid,
117
+ name: 'Expired',
118
+ exit_uuid: expiredExitUuid
119
+ }
120
+ ];
121
+
122
+ const exits = [
123
+ {
124
+ uuid: completeExitUuid,
125
+ destination_uuid: existingCompleteExit?.destination_uuid || null
126
+ },
127
+ {
128
+ uuid: expiredExitUuid,
129
+ destination_uuid: existingExpiredExit?.destination_uuid || null
130
+ }
131
+ ];
132
+
133
+ const cases = [
134
+ {
135
+ uuid: completeCaseUuid,
136
+ type: 'has_only_text',
137
+ arguments: ['completed'],
138
+ category_uuid: completeCategoryUuid
139
+ }
140
+ ];
141
+
142
+ // Create the router
143
+ const router = {
144
+ type: 'switch' as const,
145
+ categories: categories,
146
+ default_category_uuid: expiredCategoryUuid,
147
+ operand: '@child.status',
148
+ cases: cases
149
+ };
150
+
151
+ // Return the complete node
152
+ return {
153
+ uuid: originalNode.uuid,
154
+ actions: [enterFlowAction],
155
+ router: router,
156
+ exits: exits
157
+ };
158
+ }
9
159
  };
@@ -1,19 +1,142 @@
1
- import { open_ticket } from '../actions/open_ticket';
2
- import { NodeConfig } from '../types';
1
+ import { COLORS, NodeConfig } from '../types';
2
+ import { Node, OpenTicket } from '../../store/flow-definition';
3
+ import { generateUUID, createSuccessFailureRouter } from '../../utils';
4
+ import { html } from 'lit';
3
5
 
4
6
  export const split_by_ticket: NodeConfig = {
5
7
  type: 'split_by_ticket',
6
- action: open_ticket,
7
- router: {
8
- type: 'switch',
9
- defaultCategory: 'Failure',
10
- operand: '@locals._new_ticket',
11
- rules: [
8
+ name: 'Open Ticket',
9
+ color: COLORS.create,
10
+ form: {
11
+ topic: {
12
+ type: 'select',
13
+ label: 'Topic',
14
+ required: true,
15
+ placeholder: 'Select a topic',
16
+ options: [],
17
+ endpoint: '/api/v2/topics.json',
18
+ valueKey: 'uuid',
19
+ nameKey: 'name',
20
+ maxWidth: '200px'
21
+ },
22
+ assignee: {
23
+ type: 'select',
24
+ label: 'Assignee',
25
+ required: false,
26
+ placeholder: 'Select an agent (optional)',
27
+ options: [],
28
+ endpoint: '/api/v2/users.json',
29
+ valueKey: 'uuid',
30
+ getName: (item: {
31
+ first_name?: string;
32
+ last_name?: string;
33
+ name?: string;
34
+ }) => {
35
+ return item.name || [item.first_name, item.last_name].join(' ');
36
+ },
37
+ clearable: true
38
+ },
39
+ note: {
40
+ type: 'textarea',
41
+ label: 'Note',
42
+ required: false,
43
+ placeholder: 'Enter a note for the ticket (optional)',
44
+ minHeight: 100
45
+ }
46
+ },
47
+ layout: [{ type: 'row', items: ['topic', 'assignee'] }, 'note'],
48
+ render: (node: Node) => {
49
+ const openTicketAction = node.actions?.find(
50
+ (action) => action.type === 'open_ticket'
51
+ ) as OpenTicket;
52
+ return html`
53
+ <div class="body">
54
+ ${openTicketAction?.topic?.name || 'Configure ticket'}
55
+ </div>
56
+ `;
57
+ },
58
+ toFormData: (node: Node) => {
59
+ // Extract data from the existing node structure
60
+ const openTicketAction = node.actions?.find(
61
+ (action) => action.type === 'open_ticket'
62
+ ) as any;
63
+
64
+ return {
65
+ uuid: node.uuid,
66
+ topic: openTicketAction?.topic
67
+ ? [
68
+ {
69
+ uuid: openTicketAction.topic.uuid,
70
+ name: openTicketAction.topic.name
71
+ }
72
+ ]
73
+ : [],
74
+ assignee: openTicketAction?.assignee
75
+ ? [
76
+ {
77
+ uuid: openTicketAction.assignee.uuid,
78
+ name: openTicketAction.assignee.name
79
+ }
80
+ ]
81
+ : [],
82
+ note: openTicketAction?.note || ''
83
+ };
84
+ },
85
+ fromFormData: (formData: any, originalNode: Node): Node => {
86
+ // Find existing open_ticket action to preserve its UUID
87
+ const existingOpenTicketAction = originalNode.actions?.find(
88
+ (action) => action.type === 'open_ticket'
89
+ );
90
+ const openTicketUuid = existingOpenTicketAction?.uuid || generateUUID();
91
+
92
+ // Create open_ticket action
93
+ const openTicketAction: OpenTicket = {
94
+ type: 'open_ticket',
95
+ uuid: openTicketUuid,
96
+ topic:
97
+ formData.topic && formData.topic.length > 0
98
+ ? {
99
+ uuid: formData.topic[0].uuid,
100
+ name: formData.topic[0].name
101
+ }
102
+ : undefined,
103
+ assignee:
104
+ formData.assignee && formData.assignee.length > 0
105
+ ? {
106
+ uuid: formData.assignee[0].uuid,
107
+ name:
108
+ formData.assignee[0].name ||
109
+ [
110
+ formData.assignee[0].first_name,
111
+ formData.assignee[0].last_name
112
+ ].join(' ')
113
+ }
114
+ : undefined,
115
+ note: formData.note || ''
116
+ };
117
+
118
+ // Create categories and exits for Success and Failure
119
+ const existingCategories = originalNode.router?.categories || [];
120
+ const existingExits = originalNode.exits || [];
121
+ const existingCases = originalNode.router?.cases || [];
122
+
123
+ const { router, exits } = createSuccessFailureRouter(
124
+ '@locals._new_ticket',
12
125
  {
13
126
  type: 'has_text',
14
- arguments: [],
15
- categoryName: 'Success'
16
- }
17
- ]
127
+ arguments: []
128
+ },
129
+ existingCategories,
130
+ existingExits,
131
+ existingCases
132
+ );
133
+
134
+ // Return the complete node
135
+ return {
136
+ uuid: originalNode.uuid,
137
+ actions: [openTicketAction],
138
+ router: router,
139
+ exits: exits
140
+ };
18
141
  }
19
142
  };
@@ -1,19 +1,194 @@
1
- import { call_webhook } from '../actions/call_webhook';
2
- import { NodeConfig } from '../types';
1
+ import { COLORS, NodeConfig } from '../types';
2
+ import { CallWebhook, Node } from '../../store/flow-definition';
3
+ import { generateUUID, createSuccessFailureRouter } from '../../utils';
4
+ import { html } from 'lit';
5
+
6
+ const defaultPost = `@(json(object(
7
+ "contact", object(
8
+ "uuid", contact.uuid,
9
+ "name", contact.name,
10
+ "urn", contact.urn
11
+ ),
12
+ "flow", object(
13
+ "uuid", run.flow.uuid,
14
+ "name", run.flow.name
15
+ ),
16
+ "results", foreach_value(results, extract_object, "value", "category")
17
+ )))`;
3
18
 
4
19
  export const split_by_webhook: NodeConfig = {
5
20
  type: 'split_by_webhook',
6
- action: call_webhook,
7
- router: {
8
- type: 'switch',
9
- defaultCategory: 'Failure',
10
- operand: '@webhook.status',
11
- rules: [
21
+ name: 'Call Webhook',
22
+ color: COLORS.call,
23
+ form: {
24
+ method: {
25
+ type: 'select',
26
+ required: true,
27
+ options: ['GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'PATCH'],
28
+ maxWidth: '120px',
29
+ searchable: false
30
+ },
31
+ url: {
32
+ type: 'text',
33
+ required: true,
34
+ evaluated: true,
35
+ placeholder: 'https://example.com/webhook'
36
+ },
37
+ headers: {
38
+ type: 'key-value',
39
+ sortable: true,
40
+ keyPlaceholder: 'Header name',
41
+ valuePlaceholder: 'Header value',
42
+ minRows: 0
43
+ },
44
+ body: {
45
+ type: 'textarea',
46
+ evaluated: true,
47
+ placeholder: 'Request body content (JSON, XML, etc.)',
48
+ minHeight: 200,
49
+ dependsOn: ['method'],
50
+ computeValue: (
51
+ values: Record<string, any>,
52
+ currentValue: any,
53
+ originalValues?: Record<string, any>
54
+ ) => {
55
+ // Check if method is POST (handle both string and select object formats)
56
+ const method =
57
+ Array.isArray(values.method) && values.method.length > 0
58
+ ? values.method[0].value || values.method[0].name
59
+ : values.method;
60
+
61
+ if (method === 'POST') {
62
+ // For POST, provide the template if body is empty or was never set by user
63
+ if (!currentValue || currentValue.trim() === '') {
64
+ return defaultPost;
65
+ }
66
+ } else {
67
+ // For non-POST methods, clear the body if it was auto-generated or empty
68
+ // Check if the original body was empty (user never specified a body)
69
+ const originalBody = originalValues?.body || '';
70
+ const isOriginallyEmpty = !originalBody || originalBody.trim() === '';
71
+
72
+ // Clear if: originally empty, contains default template, or is currently empty
73
+ if (
74
+ isOriginallyEmpty ||
75
+ !currentValue ||
76
+ currentValue.trim() === '' ||
77
+ currentValue.trim() === defaultPost.trim()
78
+ ) {
79
+ return '';
80
+ }
81
+ }
82
+
83
+ return currentValue; // Keep existing value if user has customized it
84
+ }
85
+ }
86
+ },
87
+ layout: [
88
+ // Row with method and URL side by side
89
+ { type: 'row', items: ['method', 'url'] },
90
+ // Advanced group with nested layouts
91
+ {
92
+ type: 'group',
93
+ label: 'Headers',
94
+ items: ['headers'],
95
+ collapsible: true,
96
+ collapsed: true,
97
+ helpText: 'Configure authentication or custom headers',
98
+ getGroupValueCount: (formData: any) => {
99
+ return formData.headers?.length || 0;
100
+ }
101
+ },
102
+ {
103
+ type: 'group',
104
+ label: 'Body',
105
+ items: ['body'],
106
+ collapsible: true,
107
+ collapsed: true,
108
+ helpText: 'Configure the request payload',
109
+ getGroupValueCount: (formData: any) => {
110
+ return !!(
111
+ formData.body &&
112
+ formData.body.trim() !== '' &&
113
+ formData.body !== defaultPost
114
+ );
115
+ }
116
+ }
117
+ ],
118
+ render: (node: Node) => {
119
+ const callWebhookAction = node.actions?.find(
120
+ (action) => action.type === 'call_webhook'
121
+ ) as CallWebhook;
122
+ return html`
123
+ <div
124
+ class="body"
125
+ style="word-wrap: break-word; overflow-wrap: break-word; hyphens: auto;"
126
+ >
127
+ ${callWebhookAction?.url || 'Configure webhook'}
128
+ </div>
129
+ `;
130
+ },
131
+ toFormData: (node: Node) => {
132
+ // Extract data from the existing node structure
133
+ const callWebhookAction = node.actions?.find(
134
+ (action) => action.type === 'call_webhook'
135
+ ) as any;
136
+
137
+ return {
138
+ uuid: node.uuid,
139
+ method: callWebhookAction?.method
140
+ ? [{ value: callWebhookAction.method, name: callWebhookAction.method }]
141
+ : [{ value: 'GET', name: 'GET' }],
142
+ url: callWebhookAction?.url || '',
143
+ headers: callWebhookAction?.headers || [],
144
+ body: callWebhookAction?.body || ''
145
+ };
146
+ },
147
+ fromFormData: (formData: any, originalNode: Node): Node => {
148
+ // Get method selection
149
+ const methodSelection =
150
+ Array.isArray(formData.method) && formData.method.length > 0
151
+ ? formData.method[0]
152
+ : { value: 'GET', name: 'GET' };
153
+
154
+ // Find existing call_webhook action to preserve its UUID
155
+ const existingCallWebhookAction = originalNode.actions?.find(
156
+ (action) => action.type === 'call_webhook'
157
+ );
158
+ const callWebhookUuid = existingCallWebhookAction?.uuid || generateUUID();
159
+
160
+ // Create call_webhook action
161
+ const callWebhookAction: CallWebhook = {
162
+ type: 'call_webhook',
163
+ uuid: callWebhookUuid,
164
+ method: methodSelection.value,
165
+ url: formData.url || '',
166
+ headers: formData.headers || [],
167
+ body: formData.body || ''
168
+ };
169
+
170
+ // Create categories and exits for Success and Failure
171
+ const existingCategories = originalNode.router?.categories || [];
172
+ const existingExits = originalNode.exits || [];
173
+ const existingCases = originalNode.router?.cases || [];
174
+
175
+ const { router, exits } = createSuccessFailureRouter(
176
+ '@webhook.status',
12
177
  {
13
178
  type: 'has_number_between',
14
- arguments: ['200', '299'],
15
- categoryName: 'Success'
16
- }
17
- ]
179
+ arguments: ['200', '299']
180
+ },
181
+ existingCategories,
182
+ existingExits,
183
+ existingCases
184
+ );
185
+
186
+ // Return the complete node
187
+ return {
188
+ uuid: originalNode.uuid,
189
+ actions: [callWebhookAction],
190
+ router: router,
191
+ exits: exits
192
+ };
18
193
  }
19
194
  };