@nyaruka/temba-components 0.129.8 → 0.129.10

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 (282) hide show
  1. package/CHANGELOG.md +37 -3
  2. package/demo/data/flows/sample-flow.json +186 -96
  3. package/demo/test-colorpicker.html +30 -0
  4. package/dist/temba-components.js +1126 -1111
  5. package/dist/temba-components.js.map +1 -1
  6. package/out-tsc/src/events.js.map +1 -1
  7. package/out-tsc/src/excellent/helpers.js +2 -2
  8. package/out-tsc/src/excellent/helpers.js.map +1 -1
  9. package/out-tsc/src/flow/CanvasNode.js +25 -7
  10. package/out-tsc/src/flow/CanvasNode.js.map +1 -1
  11. package/out-tsc/src/flow/Editor.js +11 -1
  12. package/out-tsc/src/flow/Editor.js.map +1 -1
  13. package/out-tsc/src/flow/NodeEditor.js +133 -290
  14. package/out-tsc/src/flow/NodeEditor.js.map +1 -1
  15. package/out-tsc/src/flow/actions/add_input_labels.js +40 -0
  16. package/out-tsc/src/flow/actions/add_input_labels.js.map +1 -1
  17. package/out-tsc/src/flow/actions/call_llm.js +56 -3
  18. package/out-tsc/src/flow/actions/call_llm.js.map +1 -1
  19. package/out-tsc/src/flow/actions/call_webhook.js +1 -1
  20. package/out-tsc/src/flow/actions/call_webhook.js.map +1 -1
  21. package/out-tsc/src/flow/actions/open_ticket.js +65 -3
  22. package/out-tsc/src/flow/actions/open_ticket.js.map +1 -1
  23. package/out-tsc/src/flow/actions/set_run_result.js +75 -0
  24. package/out-tsc/src/flow/actions/set_run_result.js.map +1 -1
  25. package/out-tsc/src/flow/config.js +4 -0
  26. package/out-tsc/src/flow/config.js.map +1 -1
  27. package/out-tsc/src/flow/nodes/split_by_llm_categorize.js +227 -0
  28. package/out-tsc/src/flow/nodes/split_by_llm_categorize.js.map +1 -0
  29. package/out-tsc/src/flow/nodes/split_by_ticket.js +18 -0
  30. package/out-tsc/src/flow/nodes/split_by_ticket.js.map +1 -0
  31. package/out-tsc/src/flow/nodes/wait_for_response.js +27 -1
  32. package/out-tsc/src/flow/nodes/wait_for_response.js.map +1 -1
  33. package/out-tsc/src/flow/types.js +0 -65
  34. package/out-tsc/src/flow/types.js.map +1 -1
  35. package/out-tsc/src/form/ArrayEditor.js +63 -117
  36. package/out-tsc/src/form/ArrayEditor.js.map +1 -1
  37. package/out-tsc/src/form/BaseListEditor.js +4 -3
  38. package/out-tsc/src/form/BaseListEditor.js.map +1 -1
  39. package/out-tsc/src/form/Checkbox.js +77 -24
  40. package/out-tsc/src/form/Checkbox.js.map +1 -1
  41. package/out-tsc/src/form/ColorPicker.js +28 -40
  42. package/out-tsc/src/form/ColorPicker.js.map +1 -1
  43. package/out-tsc/src/form/Completion.js +44 -53
  44. package/out-tsc/src/form/Completion.js.map +1 -1
  45. package/out-tsc/src/form/Compose.js +7 -8
  46. package/out-tsc/src/form/Compose.js.map +1 -1
  47. package/out-tsc/src/form/ContactSearch.js +3 -4
  48. package/out-tsc/src/form/ContactSearch.js.map +1 -1
  49. package/out-tsc/src/form/DatePicker.js +29 -36
  50. package/out-tsc/src/form/DatePicker.js.map +1 -1
  51. package/out-tsc/src/form/{FormField.js → FieldElement.js} +81 -53
  52. package/out-tsc/src/form/FieldElement.js.map +1 -0
  53. package/out-tsc/src/form/FieldRenderer.js +306 -0
  54. package/out-tsc/src/form/FieldRenderer.js.map +1 -0
  55. package/out-tsc/src/form/ImagePicker.js +122 -126
  56. package/out-tsc/src/form/ImagePicker.js.map +1 -1
  57. package/out-tsc/src/form/KeyValueEditor.js +41 -37
  58. package/out-tsc/src/form/KeyValueEditor.js.map +1 -1
  59. package/out-tsc/src/form/MessageEditor.js +55 -63
  60. package/out-tsc/src/form/MessageEditor.js.map +1 -1
  61. package/out-tsc/src/form/TembaSlider.js +3 -3
  62. package/out-tsc/src/form/TembaSlider.js.map +1 -1
  63. package/out-tsc/src/form/TemplateEditor.js +3 -3
  64. package/out-tsc/src/form/TemplateEditor.js.map +1 -1
  65. package/out-tsc/src/form/TextInput.js +23 -27
  66. package/out-tsc/src/form/TextInput.js.map +1 -1
  67. package/out-tsc/src/form/select/Select.js +57 -35
  68. package/out-tsc/src/form/select/Select.js.map +1 -1
  69. package/out-tsc/src/form/select/UserSelect.js +8 -9
  70. package/out-tsc/src/form/select/UserSelect.js.map +1 -1
  71. package/out-tsc/src/form/select/WorkspaceSelect.js +7 -8
  72. package/out-tsc/src/form/select/WorkspaceSelect.js.map +1 -1
  73. package/out-tsc/src/live/ContactChat.js +62 -44
  74. package/out-tsc/src/live/ContactChat.js.map +1 -1
  75. package/out-tsc/src/live/ContactFieldEditor.js.map +1 -1
  76. package/out-tsc/src/markdown.js +13 -11
  77. package/out-tsc/src/markdown.js.map +1 -1
  78. package/out-tsc/temba-modules.js +3 -2
  79. package/out-tsc/temba-modules.js.map +1 -1
  80. package/out-tsc/test/ActionHelper.js +2 -0
  81. package/out-tsc/test/ActionHelper.js.map +1 -1
  82. package/out-tsc/test/NodeHelper.js +148 -0
  83. package/out-tsc/test/NodeHelper.js.map +1 -0
  84. package/out-tsc/test/actions/call_llm.test.js +103 -0
  85. package/out-tsc/test/actions/call_llm.test.js.map +1 -0
  86. package/out-tsc/test/nodes/split_by_llm_categorize.test.js +532 -0
  87. package/out-tsc/test/nodes/split_by_llm_categorize.test.js.map +1 -0
  88. package/out-tsc/test/nodes/split_by_random.test.js +150 -0
  89. package/out-tsc/test/nodes/split_by_random.test.js.map +1 -0
  90. package/out-tsc/test/nodes/wait_for_digits.test.js +150 -0
  91. package/out-tsc/test/nodes/wait_for_digits.test.js.map +1 -0
  92. package/out-tsc/test/nodes/wait_for_response.test.js +171 -0
  93. package/out-tsc/test/nodes/wait_for_response.test.js.map +1 -0
  94. package/out-tsc/test/temba-add-input-labels.test.js +70 -0
  95. package/out-tsc/test/temba-add-input-labels.test.js.map +1 -0
  96. package/out-tsc/test/temba-checkbox.test.js +16 -0
  97. package/out-tsc/test/temba-checkbox.test.js.map +1 -1
  98. package/out-tsc/test/temba-field-renderer.test.js +296 -0
  99. package/out-tsc/test/temba-field-renderer.test.js.map +1 -0
  100. package/out-tsc/test/temba-integration-markdown.test.js +2 -4
  101. package/out-tsc/test/temba-integration-markdown.test.js.map +1 -1
  102. package/out-tsc/test/temba-markdown.test.js +1 -1
  103. package/out-tsc/test/temba-markdown.test.js.map +1 -1
  104. package/out-tsc/test/temba-node-editor.test.js +400 -0
  105. package/out-tsc/test/temba-node-editor.test.js.map +1 -1
  106. package/out-tsc/test/temba-select.test.js +6 -3
  107. package/out-tsc/test/temba-select.test.js.map +1 -1
  108. package/out-tsc/test/temba-slider.test.js +0 -1
  109. package/out-tsc/test/temba-slider.test.js.map +1 -1
  110. package/out-tsc/test/temba-webchat.test.js +1 -1
  111. package/out-tsc/test/temba-webchat.test.js.map +1 -1
  112. package/package.json +1 -1
  113. package/screenshots/truth/actions/add_contact_groups/editor/descriptive-group-names.png +0 -0
  114. package/screenshots/truth/actions/add_contact_groups/editor/long-group-names.png +0 -0
  115. package/screenshots/truth/actions/add_contact_groups/editor/many-groups.png +0 -0
  116. package/screenshots/truth/actions/call_llm/editor/information-extraction.png +0 -0
  117. package/screenshots/truth/actions/call_llm/editor/sentiment-analysis.png +0 -0
  118. package/screenshots/truth/actions/call_llm/editor/summarization.png +0 -0
  119. package/screenshots/truth/actions/call_llm/editor/translation-task.png +0 -0
  120. package/screenshots/truth/actions/call_llm/render/information-extraction.png +0 -0
  121. package/screenshots/truth/actions/call_llm/render/sentiment-analysis.png +0 -0
  122. package/screenshots/truth/actions/call_llm/render/summarization.png +0 -0
  123. package/screenshots/truth/actions/call_llm/render/translation-task.png +0 -0
  124. package/screenshots/truth/actions/remove_contact_groups/editor/cleanup-groups.png +0 -0
  125. package/screenshots/truth/actions/remove_contact_groups/editor/long-descriptive-group-names.png +0 -0
  126. package/screenshots/truth/actions/remove_contact_groups/editor/many-groups.png +0 -0
  127. package/screenshots/truth/actions/remove_contact_groups/editor/multiple-groups.png +0 -0
  128. package/screenshots/truth/actions/remove_contact_groups/editor/remove-from-all-groups.png +0 -0
  129. package/screenshots/truth/actions/remove_contact_groups/editor/single-group.png +0 -0
  130. package/screenshots/truth/actions/send_email/editor/complex-business-email.png +0 -0
  131. package/screenshots/truth/actions/send_email/editor/multiple-recipients.png +0 -0
  132. package/screenshots/truth/actions/send_msg/editor/long-quick-replies.png +0 -0
  133. package/screenshots/truth/actions/send_msg/editor/multiline-text-with-replies.png +0 -0
  134. package/screenshots/truth/actions/send_msg/editor/simple-text.png +0 -0
  135. package/screenshots/truth/actions/send_msg/editor/text-with-linebreaks.png +0 -0
  136. package/screenshots/truth/actions/send_msg/editor/text-with-many-quick-replies.png +0 -0
  137. package/screenshots/truth/actions/send_msg/editor/text-with-quick-replies.png +0 -0
  138. package/screenshots/truth/actions/send_msg/editor/text-without-quick-replies.png +0 -0
  139. package/screenshots/truth/checkbox/checkbox-label-background-hover.png +0 -0
  140. package/screenshots/truth/checkbox/checkbox-no-label-no-background-hover.png +0 -0
  141. package/screenshots/truth/checkbox/checkbox-with-help-text.png +0 -0
  142. package/screenshots/truth/checkbox/checked.png +0 -0
  143. package/screenshots/truth/checkbox/default.png +0 -0
  144. package/screenshots/truth/colorpicker/default.png +0 -0
  145. package/screenshots/truth/colorpicker/focused.png +0 -0
  146. package/screenshots/truth/colorpicker/initialized.png +0 -0
  147. package/screenshots/truth/colorpicker/selected.png +0 -0
  148. package/screenshots/truth/editor/router.png +0 -0
  149. package/screenshots/truth/editor/send_msg.png +0 -0
  150. package/screenshots/truth/editor/set_contact_language.png +0 -0
  151. package/screenshots/truth/editor/set_contact_name.png +0 -0
  152. package/screenshots/truth/editor/set_run_result.png +0 -0
  153. package/screenshots/truth/editor/wait.png +0 -0
  154. package/screenshots/truth/field-renderer/checkbox-checked.png +0 -0
  155. package/screenshots/truth/field-renderer/checkbox-unchecked.png +0 -0
  156. package/screenshots/truth/field-renderer/checkbox-with-errors.png +0 -0
  157. package/screenshots/truth/field-renderer/context-comparison.png +0 -0
  158. package/screenshots/truth/field-renderer/key-value-with-label.png +0 -0
  159. package/screenshots/truth/field-renderer/message-editor-with-label.png +0 -0
  160. package/screenshots/truth/field-renderer/select-multi.png +0 -0
  161. package/screenshots/truth/field-renderer/select-no-label.png +0 -0
  162. package/screenshots/truth/field-renderer/select-with-label.png +0 -0
  163. package/screenshots/truth/field-renderer/text-evaluated.png +0 -0
  164. package/screenshots/truth/field-renderer/text-no-label.png +0 -0
  165. package/screenshots/truth/field-renderer/text-with-errors.png +0 -0
  166. package/screenshots/truth/field-renderer/text-with-label.png +0 -0
  167. package/screenshots/truth/field-renderer/textarea-evaluated.png +0 -0
  168. package/screenshots/truth/field-renderer/textarea-with-label.png +0 -0
  169. package/screenshots/truth/integration/checkbox-markdown-errors.png +0 -0
  170. package/screenshots/truth/nodes/split_by_llm_categorize/editor/basic-categorization.png +0 -0
  171. package/screenshots/truth/nodes/split_by_llm_categorize/editor/custom-input-and-result-name.png +0 -0
  172. package/screenshots/truth/nodes/split_by_llm_categorize/editor/feedback-categorization.png +0 -0
  173. package/screenshots/truth/nodes/split_by_llm_categorize/editor/many-categories.png +0 -0
  174. package/screenshots/truth/nodes/split_by_llm_categorize/editor/minimal-categories.png +0 -0
  175. package/screenshots/truth/nodes/split_by_llm_categorize/render/basic-categorization.png +0 -0
  176. package/screenshots/truth/nodes/split_by_llm_categorize/render/custom-input-and-result-name.png +0 -0
  177. package/screenshots/truth/nodes/split_by_llm_categorize/render/feedback-categorization.png +0 -0
  178. package/screenshots/truth/nodes/split_by_llm_categorize/render/many-categories.png +0 -0
  179. package/screenshots/truth/nodes/split_by_llm_categorize/render/minimal-categories.png +0 -0
  180. package/screenshots/truth/nodes/split_by_random/editor/ab-test-multiple-variants.png +0 -0
  181. package/screenshots/truth/nodes/split_by_random/editor/sampling-split.png +0 -0
  182. package/screenshots/truth/nodes/split_by_random/editor/three-way-split.png +0 -0
  183. package/screenshots/truth/nodes/split_by_random/editor/two-bucket-split.png +0 -0
  184. package/screenshots/truth/nodes/split_by_random/render/ab-test-multiple-variants.png +0 -0
  185. package/screenshots/truth/nodes/split_by_random/render/sampling-split.png +0 -0
  186. package/screenshots/truth/nodes/split_by_random/render/three-way-split.png +0 -0
  187. package/screenshots/truth/nodes/split_by_random/render/two-bucket-split.png +0 -0
  188. package/screenshots/truth/nodes/wait_for_digits/editor/basic-digits-wait.png +0 -0
  189. package/screenshots/truth/nodes/wait_for_digits/editor/phone-number-collection.png +0 -0
  190. package/screenshots/truth/nodes/wait_for_digits/editor/single-digit-with-timeout.png +0 -0
  191. package/screenshots/truth/nodes/wait_for_digits/editor/verification-code.png +0 -0
  192. package/screenshots/truth/nodes/wait_for_digits/render/basic-digits-wait.png +0 -0
  193. package/screenshots/truth/nodes/wait_for_digits/render/phone-number-collection.png +0 -0
  194. package/screenshots/truth/nodes/wait_for_digits/render/single-digit-with-timeout.png +0 -0
  195. package/screenshots/truth/nodes/wait_for_digits/render/verification-code.png +0 -0
  196. package/screenshots/truth/nodes/wait_for_response/editor/basic-wait.png +0 -0
  197. package/screenshots/truth/nodes/wait_for_response/editor/custom-result-name.png +0 -0
  198. package/screenshots/truth/nodes/wait_for_response/editor/no-timeout.png +0 -0
  199. package/screenshots/truth/nodes/wait_for_response/editor/short-timeout.png +0 -0
  200. package/screenshots/truth/nodes/wait_for_response/render/basic-wait.png +0 -0
  201. package/screenshots/truth/nodes/wait_for_response/render/custom-result-name.png +0 -0
  202. package/screenshots/truth/nodes/wait_for_response/render/no-timeout.png +0 -0
  203. package/screenshots/truth/nodes/wait_for_response/render/short-timeout.png +0 -0
  204. package/screenshots/truth/omnibox/selected.png +0 -0
  205. package/screenshots/truth/run-list/basic.png +0 -0
  206. package/screenshots/truth/select/functions.png +0 -0
  207. package/screenshots/truth/select/multi-with-endpoint.png +0 -0
  208. package/screenshots/truth/select/search-enabled.png +0 -0
  209. package/src/events.ts +12 -6
  210. package/src/excellent/helpers.ts +2 -2
  211. package/src/flow/CanvasNode.ts +22 -1
  212. package/src/flow/Editor.ts +12 -1
  213. package/src/flow/NodeEditor.ts +186 -374
  214. package/src/flow/actions/add_input_labels.ts +45 -0
  215. package/src/flow/actions/call_llm.ts +57 -3
  216. package/src/flow/actions/call_webhook.ts +1 -1
  217. package/src/flow/actions/open_ticket.ts +74 -3
  218. package/src/flow/actions/set_run_result.ts +83 -0
  219. package/src/flow/config.ts +4 -0
  220. package/src/flow/nodes/split_by_llm_categorize.ts +277 -0
  221. package/src/flow/nodes/split_by_ticket.ts +19 -0
  222. package/src/flow/nodes/wait_for_response.ts +28 -1
  223. package/src/flow/types.ts +26 -127
  224. package/src/form/ArrayEditor.ts +79 -139
  225. package/src/form/BaseListEditor.ts +4 -4
  226. package/src/form/Checkbox.ts +81 -24
  227. package/src/form/ColorPicker.ts +31 -43
  228. package/src/form/Completion.ts +49 -56
  229. package/src/form/Compose.ts +8 -8
  230. package/src/form/ContactSearch.ts +3 -4
  231. package/src/form/DatePicker.ts +32 -38
  232. package/src/form/{FormField.ts → FieldElement.ts} +108 -55
  233. package/src/form/FieldRenderer.ts +466 -0
  234. package/src/form/ImagePicker.ts +107 -110
  235. package/src/form/KeyValueEditor.ts +43 -39
  236. package/src/form/MessageEditor.ts +61 -67
  237. package/src/form/TembaSlider.ts +3 -3
  238. package/src/form/TemplateEditor.ts +3 -3
  239. package/src/form/TextInput.ts +26 -29
  240. package/src/form/select/Select.ts +63 -37
  241. package/src/form/select/UserSelect.ts +10 -11
  242. package/src/form/select/WorkspaceSelect.ts +9 -10
  243. package/src/live/ContactChat.ts +62 -47
  244. package/src/live/ContactFieldEditor.ts +2 -2
  245. package/src/markdown.ts +19 -11
  246. package/src/store/flow-definition.d.ts +5 -2
  247. package/static/api/labels.json +31 -0
  248. package/static/api/topics.json +24 -9
  249. package/static/api/users.json +35 -16
  250. package/static/css/temba-components.css +3 -3
  251. package/stress-test.js +18 -13
  252. package/temba-modules.ts +3 -2
  253. package/test/ActionHelper.ts +2 -0
  254. package/test/NodeHelper.ts +184 -0
  255. package/test/actions/call_llm.test.ts +137 -0
  256. package/test/nodes/README.md +78 -0
  257. package/test/nodes/split_by_llm_categorize.test.ts +698 -0
  258. package/test/nodes/split_by_random.test.ts +177 -0
  259. package/test/nodes/wait_for_digits.test.ts +176 -0
  260. package/test/nodes/wait_for_response.test.ts +206 -0
  261. package/test/temba-add-input-labels.test.ts +87 -0
  262. package/test/temba-checkbox.test.ts +26 -0
  263. package/test/temba-field-renderer.test.ts +482 -0
  264. package/test/temba-integration-markdown.test.ts +2 -4
  265. package/test/temba-markdown.test.ts +1 -1
  266. package/test/temba-node-editor.test.ts +496 -0
  267. package/test/temba-select.test.ts +6 -6
  268. package/test/temba-slider.test.ts +0 -1
  269. package/test/temba-webchat.test.ts +1 -1
  270. package/test-assets/contacts/history.json +7 -20
  271. package/test-assets/select/llms.json +18 -0
  272. package/web-dev-mock.mjs +96 -6
  273. package/web-dev-server.config.mjs +29 -7
  274. package/out-tsc/src/form/FormElement.js +0 -67
  275. package/out-tsc/src/form/FormElement.js.map +0 -1
  276. package/out-tsc/src/form/FormField.js.map +0 -1
  277. package/out-tsc/test/temba-formfield.test.js +0 -94
  278. package/out-tsc/test/temba-formfield.test.js.map +0 -1
  279. package/src/form/FormElement.ts +0 -69
  280. package/test/temba-flow-editor.test.ts.backup +0 -563
  281. package/test/temba-formfield.test.ts +0 -121
  282. package/test/temba-utils-index.test.ts.backup +0 -1737
@@ -8,5 +8,50 @@ export const add_input_labels: ActionConfig = {
8
8
  color: COLORS.update,
9
9
  render: (_node: Node, action: AddInputLabels) => {
10
10
  return html`<div>${renderNamedObjects(action.labels, 'label')}</div>`;
11
+ },
12
+
13
+ // Form-level transformations
14
+ toFormData: (action: AddInputLabels) => {
15
+ return {
16
+ labels: action.labels || [],
17
+ uuid: action.uuid
18
+ };
19
+ },
20
+ form: {
21
+ labels: {
22
+ type: 'select',
23
+ label: 'Labels',
24
+ helpText:
25
+ 'Select labels to add to the input. Type a new label name to create it.',
26
+ required: true,
27
+ options: [],
28
+ multi: true,
29
+ searchable: true,
30
+ endpoint: '/api/v2/labels.json',
31
+ valueKey: 'uuid',
32
+ nameKey: 'name',
33
+ placeholder: 'Search for labels or type to create new ones...',
34
+ createArbitraryOption: (input: string, options: any[]) => {
35
+ // Check if a label with this name already exists
36
+ const existing = options.find(
37
+ (option) =>
38
+ option.name.toLowerCase().trim() === input.toLowerCase().trim()
39
+ );
40
+ if (!existing && input.trim()) {
41
+ return {
42
+ name: input.trim(),
43
+ arbitrary: true
44
+ };
45
+ }
46
+ return null;
47
+ }
48
+ }
49
+ },
50
+ fromFormData: (formData: any): AddInputLabels => {
51
+ return {
52
+ uuid: formData.uuid,
53
+ type: 'add_input_labels',
54
+ labels: formData.labels || []
55
+ };
11
56
  }
12
57
  };
@@ -5,8 +5,62 @@ import { Node, CallLLM } from '../../store/flow-definition';
5
5
  export const call_llm: ActionConfig = {
6
6
  name: 'Call AI',
7
7
  color: COLORS.call,
8
- render: (_node: Node, _action: CallLLM) => {
9
- // This will need to be implemented based on the actual render logic
10
- return html`<div>Call AI</div>`;
8
+ render: (_node: Node, action: CallLLM) => {
9
+ return html`<div
10
+ style="word-wrap: break-word; overflow-wrap: break-word; hyphens: auto; max-width: 180px; max-height: 100px; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 5; -webkit-box-orient: vertical;"
11
+ >
12
+ ${action.instructions}
13
+ </div>`;
14
+ },
15
+ form: {
16
+ llm: {
17
+ type: 'select',
18
+ required: true,
19
+ options: [],
20
+ endpoint: '/test-assets/select/llms.json',
21
+ searchable: true,
22
+ valueKey: 'uuid',
23
+ nameKey: 'name'
24
+ },
25
+ input: {
26
+ type: 'text',
27
+ required: true,
28
+ label: 'The input the AI will process',
29
+ evaluated: true,
30
+ placeholder: '@input'
31
+ },
32
+ instructions: {
33
+ type: 'textarea',
34
+ required: true,
35
+ label: 'Tell the AI what to do with the input',
36
+ evaluated: true,
37
+ placeholder: 'Enter instructions for the AI model...',
38
+ minHeight: 130,
39
+ helpText: 'The result can be referenced as **`@locals._llm_output`**'
40
+ }
41
+ },
42
+ layout: ['llm', 'input', 'instructions'],
43
+ toFormData: (action: CallLLM) => {
44
+ return {
45
+ uuid: action.uuid,
46
+ llm: action.llm
47
+ ? [{ value: action.llm.uuid, name: action.llm.name }]
48
+ : [],
49
+ input: action.input || '@input',
50
+ instructions: action.instructions || ''
51
+ };
52
+ },
53
+ fromFormData: (data: Record<string, any>) => {
54
+ const llmSelection =
55
+ Array.isArray(data.llm) && data.llm.length > 0 ? data.llm[0] : null;
56
+ return {
57
+ uuid: data.uuid,
58
+ type: 'call_llm',
59
+ input: data.input || '@input',
60
+ llm: llmSelection
61
+ ? { uuid: llmSelection.value, name: llmSelection.name }
62
+ : { uuid: '', name: '' },
63
+ instructions: data.instructions || ''
64
+ } as CallLLM;
11
65
  }
12
66
  };
@@ -102,7 +102,7 @@ export const call_webhook: ActionConfig = {
102
102
  collapsed: true,
103
103
  helpText: 'Configure authentication or custom headers',
104
104
  getGroupValueCount: (formData: any) => {
105
- return formData.headers?.length + 10 || 0;
105
+ return formData.headers?.length || 0;
106
106
  }
107
107
  },
108
108
  {
@@ -5,8 +5,79 @@ import { Node, OpenTicket } from '../../store/flow-definition';
5
5
  export const open_ticket: ActionConfig = {
6
6
  name: 'Open Ticket',
7
7
  color: COLORS.create,
8
- render: (_node: Node, _action: OpenTicket) => {
9
- // This will need to be implemented based on the actual render logic
10
- return html`<div>Open Ticket</div>`;
8
+ render: (_node: Node, action: OpenTicket) => {
9
+ return html`<div>${action.topic.name}</div>`;
10
+ },
11
+ form: {
12
+ topic: {
13
+ type: 'select',
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
+ required: false,
25
+ placeholder: 'Select an agent (optional)',
26
+ options: [],
27
+ endpoint: '/api/v2/users.json',
28
+ valueKey: 'uuid',
29
+ getName: (item: {
30
+ first_name?: string;
31
+ last_name?: string;
32
+ name?: string;
33
+ }) => {
34
+ return item.name || [item.first_name, item.last_name].join(' ');
35
+ },
36
+ clearable: true
37
+ },
38
+ note: {
39
+ type: 'textarea',
40
+ required: false,
41
+ placeholder: 'Enter a note for the ticket (optional)',
42
+ minHeight: 100
43
+ }
44
+ },
45
+ layout: [{ type: 'row', items: ['topic', 'assignee'] }, 'note'],
46
+ toFormData: (action: OpenTicket) => {
47
+ return {
48
+ uuid: action.uuid,
49
+ topic: action.topic
50
+ ? [{ uuid: action.topic.uuid, name: action.topic.name }]
51
+ : [],
52
+ assignee: action.assignee
53
+ ? [{ uuid: action.assignee.uuid, name: action.assignee.name }]
54
+ : [],
55
+ note: action.note || ''
56
+ };
57
+ },
58
+ fromFormData: (data: Record<string, any>) => {
59
+ return {
60
+ uuid: data.uuid,
61
+ type: 'open_ticket',
62
+ topic:
63
+ data.topic && data.topic.length > 0
64
+ ? {
65
+ uuid: data.topic[0].uuid,
66
+ name: data.topic[0].name
67
+ }
68
+ : undefined,
69
+ assignee:
70
+ data.assignee && data.assignee.length > 0
71
+ ? {
72
+ uuid: data.assignee[0].uuid,
73
+ name:
74
+ data.assignee[0].name ||
75
+ [data.assignee[0].first_name, data.assignee[0].last_name].join(
76
+ ' '
77
+ )
78
+ }
79
+ : undefined,
80
+ note: data.note || ''
81
+ } as OpenTicket;
11
82
  }
12
83
  };
@@ -1,11 +1,94 @@
1
1
  import { html } from 'lit-html';
2
2
  import { ActionConfig, COLORS } from '../types';
3
3
  import { Node, SetRunResult } from '../../store/flow-definition';
4
+ import { getStore } from '../../store/Store';
4
5
 
5
6
  export const set_run_result: ActionConfig = {
6
7
  name: 'Save Flow Result',
7
8
  color: COLORS.save,
8
9
  render: (_node: Node, action: SetRunResult) => {
9
10
  return html`<div>Save ${action.value} as <b>${action.name}</b></div>`;
11
+ },
12
+ form: {
13
+ name: {
14
+ type: 'select',
15
+ label: 'Result Name',
16
+ helpText: 'Select an existing result name or type a new one',
17
+ required: true,
18
+ placeholder: 'Select or enter result name...',
19
+ createArbitraryOption: (input, options) => {
20
+ const exists = options.some(
21
+ (option: any) =>
22
+ option.value.toLowerCase() === input.toLowerCase() ||
23
+ option.name.toLowerCase() === input.toLowerCase()
24
+ );
25
+ return !exists && input.trim().length > 0
26
+ ? { value: input, name: input }
27
+ : null;
28
+ },
29
+ searchable: true,
30
+ clearable: false,
31
+ options: []
32
+ },
33
+ value: {
34
+ type: 'text',
35
+ label: 'Value',
36
+ helpText: 'The value to save for this result (can use expressions)',
37
+ required: false,
38
+ evaluated: true,
39
+ placeholder: 'Enter value...'
40
+ },
41
+ category: {
42
+ type: 'text',
43
+ label: 'Category',
44
+ helpText: 'Optional category for this result',
45
+ required: false,
46
+ placeholder: 'Enter category...'
47
+ }
48
+ },
49
+ layout: ['name', 'value', 'category'],
50
+ toFormData: (action: SetRunResult) => {
51
+ // Get existing flow results to populate the select options
52
+ const store = getStore();
53
+ const flowResults = store ? store.getState().getFlowResults() : [];
54
+
55
+ // Update the form configuration with dynamic options
56
+ const config = set_run_result;
57
+ if (config.form && config.form.name && config.form.name.type === 'select') {
58
+ (config.form.name as any).options = flowResults.map(
59
+ (result) => result.name
60
+ );
61
+ }
62
+
63
+ return {
64
+ uuid: action.uuid,
65
+ name: action.name || '',
66
+ value: action.value || '',
67
+ category: action.category || ''
68
+ };
69
+ },
70
+ fromFormData: (formData: any): SetRunResult => {
71
+ // Ensure name is a simple string, handling both direct values and select option objects
72
+ let name = formData.name || '';
73
+ if (Array.isArray(name) && name.length > 0) {
74
+ // If it's an array (from multi-select), take the first item
75
+ name = name[0];
76
+ }
77
+ if (typeof name === 'object' && name.value) {
78
+ // If it's an option object, extract the value
79
+ name = name.value;
80
+ }
81
+ if (typeof name === 'object' && name.name) {
82
+ // If it's an option object with name property, extract it
83
+ name = name.name;
84
+ }
85
+
86
+ return {
87
+ uuid: formData.uuid,
88
+ type: 'set_run_result',
89
+ name: String(name), // Ensure it's always a string
90
+ value: formData.value || '',
91
+ category: formData.category || ''
92
+ };
10
93
  }
11
94
  };
@@ -38,7 +38,9 @@ import { split_by_random } from './nodes/split_by_random';
38
38
  import { split_by_run_result } from './nodes/split_by_run_result';
39
39
  import { split_by_scheme } from './nodes/split_by_scheme';
40
40
  import { split_by_subflow } from './nodes/split_by_subflow';
41
+ import { split_by_ticket } from './nodes/split_by_ticket';
41
42
  import { split_by_webhook } from './nodes/split_by_webhook';
43
+ import { split_by_llm_categorize } from './nodes/split_by_llm_categorize';
42
44
  import { wait_for_audio } from './nodes/wait_for_audio';
43
45
  import { wait_for_digits } from './nodes/wait_for_digits';
44
46
  import { wait_for_image } from './nodes/wait_for_image';
@@ -84,10 +86,12 @@ export const NODE_CONFIG: {
84
86
  split_by_contact_field,
85
87
  split_by_expression,
86
88
  split_by_groups,
89
+ split_by_llm_categorize,
87
90
  split_by_random,
88
91
  split_by_run_result,
89
92
  split_by_scheme,
90
93
  split_by_subflow,
94
+ split_by_ticket,
91
95
  split_by_webhook,
92
96
  wait_for_audio,
93
97
  wait_for_digits,
@@ -0,0 +1,277 @@
1
+ import { COLORS, NodeConfig } from '../types';
2
+ import { Node } from '../../store/flow-definition';
3
+ import { generateUUID } from '../../utils';
4
+ import { html } from 'lit';
5
+
6
+ export const split_by_llm_categorize: NodeConfig = {
7
+ type: 'split_by_llm_categorize',
8
+ name: 'Split by AI',
9
+ color: COLORS.call,
10
+ form: {
11
+ llm: {
12
+ type: 'select',
13
+ label: 'LLM',
14
+ helpText: 'Select the LLM to use for categorization',
15
+ required: true,
16
+ endpoint: '/test-assets/select/llms.json',
17
+ valueKey: 'uuid',
18
+ nameKey: 'name',
19
+ placeholder: 'Select an LLM...'
20
+ },
21
+ input: {
22
+ type: 'text',
23
+ label: 'Input',
24
+ helpText: 'The input to categorize (usually @input)',
25
+ required: true,
26
+ evaluated: true,
27
+ placeholder: '@input'
28
+ },
29
+ categories: {
30
+ type: 'array',
31
+ label: 'Categories',
32
+ helpText: 'Define the categories for classification',
33
+ required: true,
34
+ itemLabel: 'Category',
35
+ minItems: 1,
36
+ maxItems: 10,
37
+ isEmptyItem: (item: any) => {
38
+ return !item.name || item.name.trim() === '';
39
+ },
40
+ itemConfig: {
41
+ name: {
42
+ type: 'text',
43
+ placeholder: 'Category name',
44
+ required: true
45
+ }
46
+ }
47
+ }
48
+ },
49
+ layout: ['llm', 'input', 'categories'],
50
+ validate: (formData: any) => {
51
+ const errors: { [key: string]: string } = {};
52
+
53
+ // Check for duplicate category names
54
+ if (formData.categories && Array.isArray(formData.categories)) {
55
+ const categories = formData.categories.filter(
56
+ (item: any) => item?.name && item.name.trim() !== ''
57
+ );
58
+
59
+ // Find all categories that have duplicates (case-insensitive)
60
+ const duplicateCategories = [];
61
+ const lowerCaseMap = new Map();
62
+
63
+ // First pass: map lowercase names to all original cases
64
+ categories.forEach((category) => {
65
+ const lowerName = category.name.trim().toLowerCase();
66
+ if (!lowerCaseMap.has(lowerName)) {
67
+ lowerCaseMap.set(lowerName, []);
68
+ }
69
+ lowerCaseMap.get(lowerName).push(category.name.trim());
70
+ });
71
+
72
+ // Second pass: collect all names that appear more than once
73
+ lowerCaseMap.forEach((originalNames) => {
74
+ if (originalNames.length > 1) {
75
+ duplicateCategories.push(...originalNames);
76
+ }
77
+ });
78
+
79
+ if (duplicateCategories.length > 0) {
80
+ const uniqueDuplicates = [...new Set(duplicateCategories)];
81
+ errors.categories = `Duplicate category names found: ${uniqueDuplicates.join(
82
+ ', '
83
+ )}`;
84
+ }
85
+ }
86
+
87
+ return {
88
+ valid: Object.keys(errors).length === 0,
89
+ errors
90
+ };
91
+ },
92
+ render: (node: Node) => {
93
+ const callLlmAction = node.actions?.find(
94
+ (action) => action.type === 'call_llm'
95
+ ) as any;
96
+ return html`
97
+ <div class="body">Categorize with ${callLlmAction.llm.name}</div>
98
+ `;
99
+ },
100
+ toFormData: (node: Node) => {
101
+ // Extract data from the existing node structure
102
+ const callLlmAction = node.actions?.find(
103
+ (action) => action.type === 'call_llm'
104
+ ) as any;
105
+ const categories =
106
+ node.router?.categories
107
+ ?.filter((cat) => cat.name !== 'Other' && cat.name !== 'Failure')
108
+ .map((cat) => ({ name: cat.name })) || [];
109
+
110
+ return {
111
+ uuid: node.uuid,
112
+ llm: callLlmAction?.llm
113
+ ? [{ value: callLlmAction.llm.uuid, name: callLlmAction.llm.name }]
114
+ : [],
115
+ input: callLlmAction?.input || '@input',
116
+ categories: categories
117
+ };
118
+ },
119
+ fromFormData: (formData: any, originalNode: Node): Node => {
120
+ // Get LLM selection
121
+ const llmSelection =
122
+ Array.isArray(formData.llm) && formData.llm.length > 0
123
+ ? formData.llm[0]
124
+ : null;
125
+
126
+ // Get user categories
127
+ const userCategories = (formData.categories || [])
128
+ .filter((item: any) => item?.name?.trim())
129
+ .map((item: any) => item.name.trim());
130
+
131
+ // Find existing call_llm action to preserve its UUID
132
+ const existingCallLlmAction = originalNode.actions?.find(
133
+ (action) => action.type === 'call_llm'
134
+ );
135
+ const callLlmUuid = existingCallLlmAction?.uuid || generateUUID();
136
+
137
+ // Create call_llm action (using any type to match the example format)
138
+ const callLlmAction: any = {
139
+ type: 'call_llm',
140
+ uuid: callLlmUuid,
141
+ llm: llmSelection
142
+ ? { uuid: llmSelection.value, name: llmSelection.name }
143
+ : { uuid: '', name: '' },
144
+ instructions: `@(prompt("categorize", slice(node.categories, 0, -2)))`,
145
+ input: formData.input || '@input',
146
+ output_local: '_llm_output'
147
+ };
148
+
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
+ const existingCategories = originalNode.router?.categories || [];
156
+ const existingExits = originalNode.exits || [];
157
+ const existingCases = originalNode.router?.cases || [];
158
+
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,
192
+ 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'
224
+ );
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
+
269
+ // Return the complete node
270
+ return {
271
+ uuid: originalNode.uuid,
272
+ actions: [callLlmAction],
273
+ router: router,
274
+ exits: exits
275
+ };
276
+ }
277
+ };
@@ -0,0 +1,19 @@
1
+ import { open_ticket } from '../actions/open_ticket';
2
+ import { NodeConfig } from '../types';
3
+
4
+ export const split_by_ticket: NodeConfig = {
5
+ type: 'split_by_ticket',
6
+ action: open_ticket,
7
+ router: {
8
+ type: 'switch',
9
+ defaultCategory: 'Failure',
10
+ operand: '@locals._new_ticket',
11
+ rules: [
12
+ {
13
+ type: 'has_text',
14
+ arguments: [],
15
+ categoryName: 'Success'
16
+ }
17
+ ]
18
+ }
19
+ };
@@ -1,7 +1,34 @@
1
1
  import { COLORS, NodeConfig } from '../types';
2
+ import { Node } from '../../store/flow-definition';
2
3
 
3
4
  export const wait_for_response: NodeConfig = {
4
5
  type: 'wait_for_response',
5
6
  name: 'Wait for Response',
6
- color: COLORS.wait
7
+ color: COLORS.wait,
8
+ form: {
9
+ result_name: {
10
+ type: 'text',
11
+ label: 'Result Name',
12
+ helpText: 'The name to save the response as',
13
+ placeholder: 'response'
14
+ }
15
+ },
16
+ layout: ['timeout', 'result_name'],
17
+ toFormData: (node: Node) => {
18
+ return {
19
+ uuid: node.uuid,
20
+ result_name: node.router?.result_name || 'response'
21
+ };
22
+ },
23
+ fromFormData: (formData: any, originalNode: Node): Node => {
24
+ const router: any = {
25
+ ...originalNode.router,
26
+ result_name: formData.result_name || 'response'
27
+ };
28
+
29
+ return {
30
+ ...originalNode,
31
+ router
32
+ };
33
+ }
7
34
  };