@nyaruka/temba-components 0.129.7 → 0.129.9

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 (269) hide show
  1. package/.devcontainer/Dockerfile +11 -4
  2. package/.devcontainer/devcontainer.json +3 -2
  3. package/.github/workflows/build.yml +4 -14
  4. package/CHANGELOG.md +29 -0
  5. package/demo/components/flow/example.html +1 -1
  6. package/demo/components/message-editor/example.html +125 -0
  7. package/demo/components/textinput/completion.html +1 -0
  8. package/demo/data/flows/food-order.json +12 -21
  9. package/demo/data/flows/sample-flow.json +210 -104
  10. package/dist/temba-components.js +715 -364
  11. package/dist/temba-components.js.map +1 -1
  12. package/out-tsc/src/display/Thumbnail.js +2 -1
  13. package/out-tsc/src/display/Thumbnail.js.map +1 -1
  14. package/out-tsc/src/events.js.map +1 -1
  15. package/out-tsc/src/excellent/helpers.js +2 -2
  16. package/out-tsc/src/excellent/helpers.js.map +1 -1
  17. package/out-tsc/src/flow/CanvasNode.js +25 -7
  18. package/out-tsc/src/flow/CanvasNode.js.map +1 -1
  19. package/out-tsc/src/flow/Editor.js +11 -1
  20. package/out-tsc/src/flow/Editor.js.map +1 -1
  21. package/out-tsc/src/flow/NodeEditor.js +342 -276
  22. package/out-tsc/src/flow/NodeEditor.js.map +1 -1
  23. package/out-tsc/src/flow/actions/add_input_labels.js +40 -0
  24. package/out-tsc/src/flow/actions/add_input_labels.js.map +1 -1
  25. package/out-tsc/src/flow/actions/call_llm.js +56 -3
  26. package/out-tsc/src/flow/actions/call_llm.js.map +1 -1
  27. package/out-tsc/src/flow/actions/call_webhook.js +26 -17
  28. package/out-tsc/src/flow/actions/call_webhook.js.map +1 -1
  29. package/out-tsc/src/flow/actions/open_ticket.js +65 -3
  30. package/out-tsc/src/flow/actions/open_ticket.js.map +1 -1
  31. package/out-tsc/src/flow/actions/send_msg.js +147 -6
  32. package/out-tsc/src/flow/actions/send_msg.js.map +1 -1
  33. package/out-tsc/src/flow/actions/set_run_result.js +75 -0
  34. package/out-tsc/src/flow/actions/set_run_result.js.map +1 -1
  35. package/out-tsc/src/flow/config.js +4 -0
  36. package/out-tsc/src/flow/config.js.map +1 -1
  37. package/out-tsc/src/flow/nodes/split_by_llm_categorize.js +227 -0
  38. package/out-tsc/src/flow/nodes/split_by_llm_categorize.js.map +1 -0
  39. package/out-tsc/src/flow/nodes/split_by_ticket.js +18 -0
  40. package/out-tsc/src/flow/nodes/split_by_ticket.js.map +1 -0
  41. package/out-tsc/src/flow/nodes/wait_for_response.js +27 -1
  42. package/out-tsc/src/flow/nodes/wait_for_response.js.map +1 -1
  43. package/out-tsc/src/flow/types.js +0 -65
  44. package/out-tsc/src/flow/types.js.map +1 -1
  45. package/out-tsc/src/form/ArrayEditor.js +87 -57
  46. package/out-tsc/src/form/ArrayEditor.js.map +1 -1
  47. package/out-tsc/src/form/BaseListEditor.js +19 -4
  48. package/out-tsc/src/form/BaseListEditor.js.map +1 -1
  49. package/out-tsc/src/form/FieldRenderer.js +305 -0
  50. package/out-tsc/src/form/FieldRenderer.js.map +1 -0
  51. package/out-tsc/src/form/FormField.js +4 -4
  52. package/out-tsc/src/form/FormField.js.map +1 -1
  53. package/out-tsc/src/form/KeyValueEditor.js +1 -1
  54. package/out-tsc/src/form/KeyValueEditor.js.map +1 -1
  55. package/out-tsc/src/form/MediaPicker.js +13 -1
  56. package/out-tsc/src/form/MediaPicker.js.map +1 -1
  57. package/out-tsc/src/form/MessageEditor.js +422 -0
  58. package/out-tsc/src/form/MessageEditor.js.map +1 -0
  59. package/out-tsc/src/form/TextInput.js +13 -6
  60. package/out-tsc/src/form/TextInput.js.map +1 -1
  61. package/out-tsc/src/form/select/Select.js +52 -24
  62. package/out-tsc/src/form/select/Select.js.map +1 -1
  63. package/out-tsc/src/live/ContactChat.js +66 -15
  64. package/out-tsc/src/live/ContactChat.js.map +1 -1
  65. package/out-tsc/src/markdown.js +13 -11
  66. package/out-tsc/src/markdown.js.map +1 -1
  67. package/out-tsc/temba-modules.js +2 -0
  68. package/out-tsc/temba-modules.js.map +1 -1
  69. package/out-tsc/test/ActionHelper.js +2 -0
  70. package/out-tsc/test/ActionHelper.js.map +1 -1
  71. package/out-tsc/test/NodeHelper.js +148 -0
  72. package/out-tsc/test/NodeHelper.js.map +1 -0
  73. package/out-tsc/test/actions/call_llm.test.js +103 -0
  74. package/out-tsc/test/actions/call_llm.test.js.map +1 -0
  75. package/out-tsc/test/nodes/split_by_llm_categorize.test.js +532 -0
  76. package/out-tsc/test/nodes/split_by_llm_categorize.test.js.map +1 -0
  77. package/out-tsc/test/nodes/split_by_random.test.js +150 -0
  78. package/out-tsc/test/nodes/split_by_random.test.js.map +1 -0
  79. package/out-tsc/test/nodes/wait_for_digits.test.js +150 -0
  80. package/out-tsc/test/nodes/wait_for_digits.test.js.map +1 -0
  81. package/out-tsc/test/nodes/wait_for_response.test.js +171 -0
  82. package/out-tsc/test/nodes/wait_for_response.test.js.map +1 -0
  83. package/out-tsc/test/temba-add-input-labels.test.js +70 -0
  84. package/out-tsc/test/temba-add-input-labels.test.js.map +1 -0
  85. package/out-tsc/test/temba-field-config.test.js +4 -2
  86. package/out-tsc/test/temba-field-config.test.js.map +1 -1
  87. package/out-tsc/test/temba-field-renderer.test.js +296 -0
  88. package/out-tsc/test/temba-field-renderer.test.js.map +1 -0
  89. package/out-tsc/test/temba-markdown.test.js +1 -1
  90. package/out-tsc/test/temba-markdown.test.js.map +1 -1
  91. package/out-tsc/test/temba-message-editor.test.js +194 -0
  92. package/out-tsc/test/temba-message-editor.test.js.map +1 -0
  93. package/out-tsc/test/temba-node-editor.test.js +471 -0
  94. package/out-tsc/test/temba-node-editor.test.js.map +1 -1
  95. package/out-tsc/test/temba-select.test.js +7 -4
  96. package/out-tsc/test/temba-select.test.js.map +1 -1
  97. package/out-tsc/test/temba-textinput.test.js +16 -0
  98. package/out-tsc/test/temba-textinput.test.js.map +1 -1
  99. package/out-tsc/test/temba-webchat.test.js +5 -1
  100. package/out-tsc/test/temba-webchat.test.js.map +1 -1
  101. package/out-tsc/test/utils.test.js +2 -8
  102. package/out-tsc/test/utils.test.js.map +1 -1
  103. package/package.json +7 -4
  104. package/screenshots/truth/actions/add_contact_groups/editor/descriptive-group-names.png +0 -0
  105. package/screenshots/truth/actions/add_contact_groups/editor/long-group-names.png +0 -0
  106. package/screenshots/truth/actions/add_contact_groups/editor/many-groups.png +0 -0
  107. package/screenshots/truth/actions/add_contact_groups/editor/multiple-groups.png +0 -0
  108. package/screenshots/truth/actions/add_contact_groups/editor/single-group.png +0 -0
  109. package/screenshots/truth/actions/call_llm/editor/information-extraction.png +0 -0
  110. package/screenshots/truth/actions/call_llm/editor/sentiment-analysis.png +0 -0
  111. package/screenshots/truth/actions/call_llm/editor/summarization.png +0 -0
  112. package/screenshots/truth/actions/call_llm/editor/translation-task.png +0 -0
  113. package/screenshots/truth/actions/call_llm/render/information-extraction.png +0 -0
  114. package/screenshots/truth/actions/call_llm/render/sentiment-analysis.png +0 -0
  115. package/screenshots/truth/actions/call_llm/render/summarization.png +0 -0
  116. package/screenshots/truth/actions/call_llm/render/translation-task.png +0 -0
  117. package/screenshots/truth/actions/remove_contact_groups/editor/cleanup-groups.png +0 -0
  118. package/screenshots/truth/actions/remove_contact_groups/editor/long-descriptive-group-names.png +0 -0
  119. package/screenshots/truth/actions/remove_contact_groups/editor/many-groups.png +0 -0
  120. package/screenshots/truth/actions/remove_contact_groups/editor/multiple-groups.png +0 -0
  121. package/screenshots/truth/actions/remove_contact_groups/editor/remove-from-all-groups.png +0 -0
  122. package/screenshots/truth/actions/remove_contact_groups/editor/single-group.png +0 -0
  123. package/screenshots/truth/actions/send_email/editor/complex-business-email.png +0 -0
  124. package/screenshots/truth/actions/send_email/editor/empty-body.png +0 -0
  125. package/screenshots/truth/actions/send_email/editor/empty-subject.png +0 -0
  126. package/screenshots/truth/actions/send_email/editor/long-subject.png +0 -0
  127. package/screenshots/truth/actions/send_email/editor/multiline-body.png +0 -0
  128. package/screenshots/truth/actions/send_email/editor/multiple-recipients.png +0 -0
  129. package/screenshots/truth/actions/send_email/editor/simple-email.png +0 -0
  130. package/screenshots/truth/actions/send_email/editor/with-expressions.png +0 -0
  131. package/screenshots/truth/actions/send_msg/editor/long-quick-replies.png +0 -0
  132. package/screenshots/truth/actions/send_msg/editor/multiline-text-with-replies.png +0 -0
  133. package/screenshots/truth/actions/send_msg/editor/simple-text.png +0 -0
  134. package/screenshots/truth/actions/send_msg/editor/text-with-linebreaks.png +0 -0
  135. package/screenshots/truth/actions/send_msg/editor/text-with-many-quick-replies.png +0 -0
  136. package/screenshots/truth/actions/send_msg/editor/text-with-quick-replies.png +0 -0
  137. package/screenshots/truth/actions/send_msg/editor/text-without-quick-replies.png +0 -0
  138. package/screenshots/truth/editor/router.png +0 -0
  139. package/screenshots/truth/editor/send_msg.png +0 -0
  140. package/screenshots/truth/editor/set_contact_language.png +0 -0
  141. package/screenshots/truth/editor/set_contact_name.png +0 -0
  142. package/screenshots/truth/editor/set_run_result.png +0 -0
  143. package/screenshots/truth/editor/wait.png +0 -0
  144. package/screenshots/truth/field-renderer/checkbox-checked.png +0 -0
  145. package/screenshots/truth/field-renderer/checkbox-unchecked.png +0 -0
  146. package/screenshots/truth/field-renderer/checkbox-with-errors.png +0 -0
  147. package/screenshots/truth/field-renderer/context-comparison.png +0 -0
  148. package/screenshots/truth/field-renderer/key-value-with-label.png +0 -0
  149. package/screenshots/truth/field-renderer/message-editor-with-label.png +0 -0
  150. package/screenshots/truth/field-renderer/select-multi.png +0 -0
  151. package/screenshots/truth/field-renderer/select-no-label.png +0 -0
  152. package/screenshots/truth/field-renderer/select-with-label.png +0 -0
  153. package/screenshots/truth/field-renderer/text-evaluated.png +0 -0
  154. package/screenshots/truth/field-renderer/text-no-label.png +0 -0
  155. package/screenshots/truth/field-renderer/text-with-errors.png +0 -0
  156. package/screenshots/truth/field-renderer/text-with-label.png +0 -0
  157. package/screenshots/truth/field-renderer/textarea-evaluated.png +0 -0
  158. package/screenshots/truth/field-renderer/textarea-with-label.png +0 -0
  159. package/screenshots/truth/formfield/markdown-errors.png +0 -0
  160. package/screenshots/truth/formfield/no-errors.png +0 -0
  161. package/screenshots/truth/formfield/plain-text-errors.png +0 -0
  162. package/screenshots/truth/message-editor/autogrow-initial-content.png +0 -0
  163. package/screenshots/truth/message-editor/default.png +0 -0
  164. package/screenshots/truth/message-editor/drag-highlight.png +0 -0
  165. package/screenshots/truth/message-editor/filtered-attachments.png +0 -0
  166. package/screenshots/truth/message-editor/with-completion.png +0 -0
  167. package/screenshots/truth/message-editor/with-properties.png +0 -0
  168. package/screenshots/truth/nodes/split_by_llm_categorize/editor/basic-categorization.png +0 -0
  169. package/screenshots/truth/nodes/split_by_llm_categorize/editor/custom-input-and-result-name.png +0 -0
  170. package/screenshots/truth/nodes/split_by_llm_categorize/editor/feedback-categorization.png +0 -0
  171. package/screenshots/truth/nodes/split_by_llm_categorize/editor/many-categories.png +0 -0
  172. package/screenshots/truth/nodes/split_by_llm_categorize/editor/minimal-categories.png +0 -0
  173. package/screenshots/truth/nodes/split_by_llm_categorize/render/basic-categorization.png +0 -0
  174. package/screenshots/truth/nodes/split_by_llm_categorize/render/custom-input-and-result-name.png +0 -0
  175. package/screenshots/truth/nodes/split_by_llm_categorize/render/feedback-categorization.png +0 -0
  176. package/screenshots/truth/nodes/split_by_llm_categorize/render/many-categories.png +0 -0
  177. package/screenshots/truth/nodes/split_by_llm_categorize/render/minimal-categories.png +0 -0
  178. package/screenshots/truth/nodes/split_by_random/editor/ab-test-multiple-variants.png +0 -0
  179. package/screenshots/truth/nodes/split_by_random/editor/sampling-split.png +0 -0
  180. package/screenshots/truth/nodes/split_by_random/editor/three-way-split.png +0 -0
  181. package/screenshots/truth/nodes/split_by_random/editor/two-bucket-split.png +0 -0
  182. package/screenshots/truth/nodes/split_by_random/render/ab-test-multiple-variants.png +0 -0
  183. package/screenshots/truth/nodes/split_by_random/render/sampling-split.png +0 -0
  184. package/screenshots/truth/nodes/split_by_random/render/three-way-split.png +0 -0
  185. package/screenshots/truth/nodes/split_by_random/render/two-bucket-split.png +0 -0
  186. package/screenshots/truth/nodes/wait_for_digits/editor/basic-digits-wait.png +0 -0
  187. package/screenshots/truth/nodes/wait_for_digits/editor/phone-number-collection.png +0 -0
  188. package/screenshots/truth/nodes/wait_for_digits/editor/single-digit-with-timeout.png +0 -0
  189. package/screenshots/truth/nodes/wait_for_digits/editor/verification-code.png +0 -0
  190. package/screenshots/truth/nodes/wait_for_digits/render/basic-digits-wait.png +0 -0
  191. package/screenshots/truth/nodes/wait_for_digits/render/phone-number-collection.png +0 -0
  192. package/screenshots/truth/nodes/wait_for_digits/render/single-digit-with-timeout.png +0 -0
  193. package/screenshots/truth/nodes/wait_for_digits/render/verification-code.png +0 -0
  194. package/screenshots/truth/nodes/wait_for_response/editor/basic-wait.png +0 -0
  195. package/screenshots/truth/nodes/wait_for_response/editor/custom-result-name.png +0 -0
  196. package/screenshots/truth/nodes/wait_for_response/editor/no-timeout.png +0 -0
  197. package/screenshots/truth/nodes/wait_for_response/editor/short-timeout.png +0 -0
  198. package/screenshots/truth/nodes/wait_for_response/render/basic-wait.png +0 -0
  199. package/screenshots/truth/nodes/wait_for_response/render/custom-result-name.png +0 -0
  200. package/screenshots/truth/nodes/wait_for_response/render/no-timeout.png +0 -0
  201. package/screenshots/truth/nodes/wait_for_response/render/short-timeout.png +0 -0
  202. package/screenshots/truth/omnibox/selected.png +0 -0
  203. package/screenshots/truth/select/functions.png +0 -0
  204. package/screenshots/truth/select/multi-with-endpoint.png +0 -0
  205. package/screenshots/truth/select/search-enabled.png +0 -0
  206. package/screenshots/truth/textinput/autogrow-initial.png +0 -0
  207. package/screenshots/truth/textinput/input-form.png +0 -0
  208. package/src/display/Thumbnail.ts +2 -1
  209. package/src/events.ts +13 -1
  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 +412 -354
  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 +28 -18
  217. package/src/flow/actions/open_ticket.ts +74 -3
  218. package/src/flow/actions/send_msg.ts +170 -6
  219. package/src/flow/actions/set_run_result.ts +83 -0
  220. package/src/flow/config.ts +4 -0
  221. package/src/flow/nodes/split_by_llm_categorize.ts +277 -0
  222. package/src/flow/nodes/split_by_ticket.ts +19 -0
  223. package/src/flow/nodes/wait_for_response.ts +28 -1
  224. package/src/flow/types.ts +46 -128
  225. package/src/form/ArrayEditor.ts +96 -66
  226. package/src/form/BaseListEditor.ts +22 -6
  227. package/src/form/FieldRenderer.ts +465 -0
  228. package/src/form/FormField.ts +4 -4
  229. package/src/form/KeyValueEditor.ts +1 -1
  230. package/src/form/MediaPicker.ts +13 -1
  231. package/src/form/MessageEditor.ts +449 -0
  232. package/src/form/TextInput.ts +16 -8
  233. package/src/form/select/Select.ts +55 -24
  234. package/src/live/ContactChat.ts +69 -19
  235. package/src/markdown.ts +19 -11
  236. package/src/store/flow-definition.d.ts +5 -2
  237. package/static/api/labels.json +31 -0
  238. package/static/api/topics.json +24 -9
  239. package/static/api/users.json +35 -16
  240. package/static/css/temba-components.css +5 -3
  241. package/static/mr/docs/en-us/editor.json +2588 -0
  242. package/stress-test.js +143 -0
  243. package/temba-modules.ts +2 -0
  244. package/test/ActionHelper.ts +2 -0
  245. package/test/NodeHelper.ts +184 -0
  246. package/test/actions/call_llm.test.ts +137 -0
  247. package/test/nodes/README.md +78 -0
  248. package/test/nodes/split_by_llm_categorize.test.ts +698 -0
  249. package/test/nodes/split_by_random.test.ts +177 -0
  250. package/test/nodes/wait_for_digits.test.ts +176 -0
  251. package/test/nodes/wait_for_response.test.ts +206 -0
  252. package/test/temba-add-input-labels.test.ts +87 -0
  253. package/test/temba-field-config.test.ts +4 -2
  254. package/test/temba-field-renderer.test.ts +482 -0
  255. package/test/temba-markdown.test.ts +1 -1
  256. package/test/temba-message-editor.test.ts +300 -0
  257. package/test/temba-node-editor.test.ts +590 -0
  258. package/test/temba-select.test.ts +7 -7
  259. package/test/temba-textinput.test.ts +26 -0
  260. package/test/temba-webchat.test.ts +6 -1
  261. package/test/utils.test.ts +2 -13
  262. package/test-assets/contacts/history.json +19 -0
  263. package/test-assets/select/llms.json +18 -0
  264. package/test-assets/style.css +2 -0
  265. package/web-dev-mock.mjs +523 -0
  266. package/web-dev-server.config.mjs +74 -6
  267. package/web-test-runner.config.mjs +9 -4
  268. package/test/temba-flow-editor.test.ts.backup +0 -563
  269. 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
  };
@@ -2,6 +2,19 @@ import { html } from 'lit-html';
2
2
  import { ActionConfig, COLORS } from '../types';
3
3
  import { Node, CallWebhook } from '../../store/flow-definition';
4
4
 
5
+ const defaultPost = `@(json(object(
6
+ "contact", object(
7
+ "uuid", contact.uuid,
8
+ "name", contact.name,
9
+ "urn", contact.urn
10
+ ),
11
+ "flow", object(
12
+ "uuid", run.flow.uuid,
13
+ "name", run.flow.name
14
+ ),
15
+ "results", foreach_value(results, extract_object, "value", "category")
16
+ )))`;
17
+
5
18
  export const call_webhook: ActionConfig = {
6
19
  name: 'Call Webhook',
7
20
  color: COLORS.call,
@@ -19,7 +32,7 @@ export const call_webhook: ActionConfig = {
19
32
  required: true,
20
33
  options: ['GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'PATCH'],
21
34
  maxWidth: '120px',
22
- searchable: true
35
+ searchable: false
23
36
  },
24
37
  url: {
25
38
  type: 'text',
@@ -51,23 +64,10 @@ export const call_webhook: ActionConfig = {
51
64
  ? values.method[0].value || values.method[0].name
52
65
  : values.method;
53
66
 
54
- const defaultTemplate = `@(json(object(
55
- "contact", object(
56
- "uuid", contact.uuid,
57
- "name", contact.name,
58
- "urn", contact.urn
59
- ),
60
- "flow", object(
61
- "uuid", run.flow.uuid,
62
- "name", run.flow.name
63
- ),
64
- "results", foreach_value(results, extract_object, "value", "category")
65
- )))`;
66
-
67
67
  if (method === 'POST') {
68
68
  // For POST, provide the template if body is empty or was never set by user
69
69
  if (!currentValue || currentValue.trim() === '') {
70
- return defaultTemplate;
70
+ return defaultPost;
71
71
  }
72
72
  } else {
73
73
  // For non-POST methods, clear the body if it was auto-generated or empty
@@ -80,7 +80,7 @@ export const call_webhook: ActionConfig = {
80
80
  isOriginallyEmpty ||
81
81
  !currentValue ||
82
82
  currentValue.trim() === '' ||
83
- currentValue.trim() === defaultTemplate.trim()
83
+ currentValue.trim() === defaultPost.trim()
84
84
  ) {
85
85
  return '';
86
86
  }
@@ -100,7 +100,10 @@ export const call_webhook: ActionConfig = {
100
100
  items: ['headers'],
101
101
  collapsible: true,
102
102
  collapsed: true,
103
- helpText: 'Configure authentication or custom headers'
103
+ helpText: 'Configure authentication or custom headers',
104
+ getGroupValueCount: (formData: any) => {
105
+ return formData.headers?.length || 0;
106
+ }
104
107
  },
105
108
  {
106
109
  type: 'group',
@@ -108,7 +111,14 @@ export const call_webhook: ActionConfig = {
108
111
  items: ['body'],
109
112
  collapsible: true,
110
113
  collapsed: true,
111
- helpText: 'Configure the request payload'
114
+ helpText: 'Configure the request payload',
115
+ getGroupValueCount: (formData: any) => {
116
+ return !!(
117
+ formData.body &&
118
+ formData.body.trim() !== '' &&
119
+ formData.body !== defaultPost
120
+ );
121
+ }
112
122
  }
113
123
  ],
114
124
  toFormData: (action: CallWebhook) => {
@@ -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
  };
@@ -29,18 +29,22 @@ export const send_msg: ActionConfig = {
29
29
  },
30
30
  form: {
31
31
  text: {
32
- type: 'textarea',
33
- label: 'Message Text',
32
+ type: 'message-editor',
33
+ label: 'Message',
34
34
  helpText:
35
- 'Enter the message to send. You can use expressions like @contact.name',
35
+ 'Enter the message to send with optional attachments. You can use expressions like @contact.name',
36
36
  required: true,
37
37
  evaluated: true,
38
- minHeight: 175
38
+ placeholder: 'Type your message here...',
39
+ maxAttachments: 10,
40
+ accept: '',
41
+ endpoint: '/api/v2/media.json',
42
+ counter: 'temba-charcount',
43
+ gsm: true,
44
+ autogrow: true
39
45
  },
40
46
  quick_replies: {
41
47
  type: 'select',
42
- label: 'Quick Replies',
43
- helpText: 'Add quick reply options for this message',
44
48
  options: [],
45
49
  multi: true,
46
50
  tags: true,
@@ -48,6 +52,137 @@ export const send_msg: ActionConfig = {
48
52
  placeholder: 'Add quick replies...',
49
53
  maxItems: 10,
50
54
  evaluated: true
55
+ },
56
+ runtime_attachments: {
57
+ type: 'array',
58
+ helpText: 'Add dynamic attachments using expressions',
59
+ itemLabel: 'Attachment',
60
+ maxItems: 10,
61
+ isEmptyItem: (item: any) => {
62
+ return !item.expression || item.expression.trim() === '';
63
+ },
64
+ itemConfig: {
65
+ type: {
66
+ type: 'select',
67
+ options: [
68
+ { value: 'image', label: 'Image' },
69
+ { value: 'audio', label: 'Audio' },
70
+ { value: 'video', label: 'Video' },
71
+ { value: 'document', label: 'Document' }
72
+ ],
73
+ required: true,
74
+ searchable: false
75
+ },
76
+ expression: {
77
+ type: 'text',
78
+ placeholder: 'Expression (e.g. @contact.photo)',
79
+ required: true,
80
+ evaluated: true
81
+ }
82
+ }
83
+ }
84
+ },
85
+ layout: [
86
+ 'text',
87
+ {
88
+ type: 'group',
89
+ label: 'Quick Replies',
90
+ items: ['quick_replies'],
91
+ collapsible: true,
92
+ collapsed: (formData: any) => {
93
+ // Collapse only if there are no quick replies
94
+ return !formData.quick_replies || formData.quick_replies.length === 0;
95
+ },
96
+ getGroupValueCount: (formData: any) => {
97
+ return formData.quick_replies?.length || 0;
98
+ }
99
+ },
100
+ {
101
+ type: 'group',
102
+ label: 'Runtime Attachments',
103
+ items: ['runtime_attachments'],
104
+ collapsible: true,
105
+ collapsed: true,
106
+ helpText: 'Add dynamic attachments that are evaluated at runtime',
107
+ getGroupValueCount: (formData: any) => {
108
+ return (
109
+ formData.runtime_attachments?.filter(
110
+ (item: any) =>
111
+ item && item.expression && item.expression.trim() !== ''
112
+ ).length || 0
113
+ );
114
+ }
115
+ }
116
+ ],
117
+ toFormData: (action: SendMsg) => {
118
+ // Extract runtime attachments from the text field attachments
119
+ const runtimeAttachments: { type: string; expression: string }[] = [];
120
+ const staticAttachments: string[] = [];
121
+
122
+ if (action.attachments && Array.isArray(action.attachments)) {
123
+ action.attachments.forEach((attachment) => {
124
+ if (typeof attachment === 'string' && attachment.includes(':')) {
125
+ const colonIndex = attachment.indexOf(':');
126
+ const contentType = attachment.substring(0, colonIndex);
127
+ const value = attachment.substring(colonIndex + 1);
128
+
129
+ if (!contentType.includes('/')) {
130
+ // This is a runtime attachment
131
+ runtimeAttachments.push({
132
+ type: contentType,
133
+ expression: value
134
+ });
135
+ } else {
136
+ // This is a static attachment
137
+ staticAttachments.push(attachment);
138
+ }
139
+ }
140
+ });
141
+ }
142
+
143
+ return {
144
+ uuid: action.uuid,
145
+ text: action.text || '',
146
+ attachments: staticAttachments,
147
+ runtime_attachments: runtimeAttachments,
148
+ quick_replies: (action.quick_replies || []).map((reply) => ({
149
+ name: reply,
150
+ value: reply
151
+ }))
152
+ };
153
+ },
154
+ fromFormData: (data: Record<string, any>) => {
155
+ const result = {
156
+ uuid: data.uuid,
157
+ type: 'send_msg',
158
+ text: data.text || '',
159
+ attachments: [],
160
+ quick_replies: (data.quick_replies || []).map((reply: any) =>
161
+ typeof reply === 'string' ? reply : reply.value || reply.name || reply
162
+ )
163
+ };
164
+
165
+ // Combine static attachments from text field with runtime attachments
166
+ const staticAttachments = data.attachments || [];
167
+ const runtimeAttachments = (data.runtime_attachments || [])
168
+ .filter((item: any) => item && item.type && item.expression) // Filter out invalid items
169
+ .map(
170
+ (item: { type: string; expression: string }) =>
171
+ `${item.type}:${item.expression}`
172
+ );
173
+
174
+ result.attachments = [...staticAttachments, ...runtimeAttachments];
175
+
176
+ // Remove quick_replies if empty to match original format
177
+ if (result.quick_replies.length === 0) {
178
+ delete (result as any).quick_replies;
179
+ }
180
+
181
+ return result as SendMsg;
182
+ },
183
+ sanitize: (formData: any): void => {
184
+ if (formData.text && typeof formData.text === 'string') {
185
+ formData.text = formData.text.trim();
51
186
  }
52
187
  },
53
188
  validate: (action: SendMsg): ValidationResult => {
@@ -57,6 +192,35 @@ export const send_msg: ActionConfig = {
57
192
  errors.text = 'Message text is required';
58
193
  }
59
194
 
195
+ const attachments = action.attachments || [];
196
+ if (attachments.length > 10) {
197
+ const staticAttachments = attachments.filter(
198
+ (attachment) =>
199
+ typeof attachment === 'string' &&
200
+ attachment.substring(0, attachment.indexOf(':')).includes('/')
201
+ );
202
+
203
+ const runtimeAttachments = attachments.filter(
204
+ (attachment) =>
205
+ typeof attachment === 'string' &&
206
+ !attachment.substring(0, attachment.indexOf(':')).includes('/')
207
+ );
208
+
209
+ if (runtimeAttachments.length > 0) {
210
+ errors.runtime_attachments =
211
+ 'Each message can only have up to 10 attachments';
212
+ }
213
+
214
+ if (staticAttachments.length > 0) {
215
+ const message = 'Each message can only have up to 10 total attachments';
216
+ if (errors.text) {
217
+ errors.text += ` ${message}`;
218
+ } else {
219
+ errors.text = message;
220
+ }
221
+ }
222
+ }
223
+
60
224
  return {
61
225
  valid: Object.keys(errors).length === 0,
62
226
  errors
@@ -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,