@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
@@ -29,6 +29,28 @@ describe('temba-node-editor', () => {
29
29
  expect(el.shadowRoot).to.not.be.null;
30
30
  expect(el.action).to.equal(action);
31
31
  });
32
+ it('renders send_msg action with message editor', async () => {
33
+ const action = {
34
+ uuid: 'test-action-uuid',
35
+ type: 'send_msg',
36
+ text: 'Hello @contact.name, check this out!',
37
+ attachments: [
38
+ 'image/jpeg:http://example.com/photo.jpg',
39
+ 'image:@fields.profile_pic'
40
+ ],
41
+ quick_replies: ['Yes', 'No']
42
+ };
43
+ const el = (await fixture(html `
44
+ <temba-node-editor .action=${action} .isOpen=${true}></temba-node-editor>
45
+ `));
46
+ await el.updateComplete;
47
+ expect(el.shadowRoot).to.not.be.null;
48
+ expect(el.action).to.equal(action);
49
+ // Check that the message editor component is rendered
50
+ const messageEditor = el.shadowRoot.querySelector('temba-message-editor');
51
+ expect(messageEditor).to.not.be.null;
52
+ expect(messageEditor.value).to.equal(action.text);
53
+ });
32
54
  it('renders set_run_result action', async () => {
33
55
  const action = {
34
56
  uuid: 'test-action-uuid',
@@ -279,5 +301,454 @@ describe('temba-node-editor', () => {
279
301
  await assertDialogScreenshot(el, `editor/${actionType.type}`);
280
302
  }
281
303
  });
304
+ it('displays bubble count for group value counts', async () => {
305
+ const action = {
306
+ uuid: 'test-action-uuid',
307
+ type: 'send_msg',
308
+ text: 'Hello world',
309
+ quick_replies: ['Yes', 'No', 'Maybe'],
310
+ attachments: ['image:@contact.photo', 'document:@contact.resume']
311
+ };
312
+ const el = (await fixture(html `
313
+ <temba-node-editor .action=${action} .isOpen=${true}></temba-node-editor>
314
+ `));
315
+ await el.updateComplete;
316
+ // Wait for form data to be fully initialized and re-render to complete
317
+ await new Promise((resolve) => setTimeout(resolve, 200));
318
+ await el.updateComplete;
319
+ // Check that bubble counts are displayed
320
+ const shadowRoot = el.shadowRoot;
321
+ const bubbles = shadowRoot.querySelectorAll('.group-count-bubble');
322
+ // Should have bubbles for groups with values
323
+ expect(bubbles.length).to.be.greaterThan(0);
324
+ // Check specific bubble values (trim to handle whitespace in rendered text)
325
+ const bubbleTexts = Array.from(bubbles).map((bubble) => { var _a; return (_a = bubble.textContent) === null || _a === void 0 ? void 0 : _a.trim(); });
326
+ // Runtime attachments group should show bubble when collapsed and has values
327
+ expect(bubbleTexts).to.include('2'); // 2 runtime attachments
328
+ // Note: Quick replies group auto-expands when it has content, so no bubble is shown
329
+ });
330
+ it('shows arrow when group has no values', async () => {
331
+ const action = {
332
+ uuid: 'test-action-uuid',
333
+ type: 'send_msg',
334
+ text: 'Hello world'
335
+ // No quick_replies or attachments provided
336
+ };
337
+ const el = (await fixture(html `
338
+ <temba-node-editor .action=${action} .isOpen=${true}></temba-node-editor>
339
+ `));
340
+ await el.updateComplete;
341
+ // Wait for form data initialization
342
+ await new Promise((resolve) => setTimeout(resolve, 200));
343
+ await el.updateComplete;
344
+ // Check that arrows are displayed instead of bubbles
345
+ const shadowRoot = el.shadowRoot;
346
+ const bubbles = shadowRoot.querySelectorAll('.group-count-bubble');
347
+ const arrows = shadowRoot.querySelectorAll('.group-toggle-icon');
348
+ // Should have no bubbles when counts are 0
349
+ expect(bubbles.length).to.equal(0);
350
+ // Should have arrows for collapsible groups
351
+ expect(arrows.length).to.be.greaterThan(0);
352
+ });
353
+ it('renders split_by_llm_categorize node', async () => {
354
+ const node = {
355
+ uuid: 'test-node-uuid',
356
+ actions: [
357
+ {
358
+ uuid: 'call-llm-uuid',
359
+ type: 'call_llm',
360
+ llm: { uuid: 'llm-123', name: 'Test LLM' },
361
+ input: '@input',
362
+ instructions: '@(prompt("categorize", slice(node.categories, 0, -2)))',
363
+ output_local: '_llm_output'
364
+ }
365
+ ],
366
+ router: {
367
+ type: 'switch',
368
+ operand: '@locals._llm_output',
369
+ result_name: 'Intent',
370
+ categories: [
371
+ { uuid: 'cat-1', name: 'Greeting', exit_uuid: 'exit-1' },
372
+ { uuid: 'cat-2', name: 'Question', exit_uuid: 'exit-2' },
373
+ { uuid: 'cat-3', name: 'Other', exit_uuid: 'exit-3' },
374
+ { uuid: 'cat-4', name: 'Failure', exit_uuid: 'exit-4' }
375
+ ]
376
+ },
377
+ exits: [
378
+ { uuid: 'exit-1', destination_uuid: null },
379
+ { uuid: 'exit-2', destination_uuid: null },
380
+ { uuid: 'exit-3', destination_uuid: null },
381
+ { uuid: 'exit-4', destination_uuid: null }
382
+ ]
383
+ };
384
+ const nodeUI = { type: 'split_by_llm_categorize' };
385
+ const el = (await fixture(html `
386
+ <temba-node-editor
387
+ .node=${node}
388
+ .nodeUI=${nodeUI}
389
+ .isOpen=${true}
390
+ ></temba-node-editor>
391
+ `));
392
+ await el.updateComplete;
393
+ expect(el.shadowRoot).to.not.be.null;
394
+ expect(el.node).to.equal(node);
395
+ expect(el.nodeUI).to.equal(nodeUI);
396
+ // Wait for form data initialization
397
+ await new Promise((resolve) => setTimeout(resolve, 200));
398
+ await el.updateComplete;
399
+ // Check if the dialog is rendered with correct header
400
+ const dialog = el.shadowRoot.querySelector('temba-dialog');
401
+ expect(dialog).to.not.be.null;
402
+ expect(dialog.getAttribute('header')).to.equal('Split by AI');
403
+ // Check that the form is rendered
404
+ const form = el.shadowRoot.querySelector('.node-editor-form');
405
+ expect(form).to.not.be.null;
406
+ // Check that all expected form components are rendered
407
+ const selectComponents = el.shadowRoot.querySelectorAll('temba-select');
408
+ const arrayComponents = el.shadowRoot.querySelectorAll('temba-array-editor');
409
+ const completionComponents = el.shadowRoot.querySelectorAll('temba-completion');
410
+ // Should have LLM select field
411
+ expect(selectComponents.length).to.equal(1);
412
+ expect(selectComponents[0].getAttribute('label')).to.equal('LLM');
413
+ // Should have input completion field
414
+ expect(completionComponents.length).to.equal(1);
415
+ expect(completionComponents[0].getAttribute('label')).to.equal('Input');
416
+ // Should have categories array editor
417
+ expect(arrayComponents.length).to.equal(1);
418
+ });
419
+ it('renders wait_for_response node', async () => {
420
+ const node = {
421
+ uuid: 'test-wait-node-uuid',
422
+ actions: [],
423
+ router: {
424
+ type: 'switch',
425
+ wait: {
426
+ type: 'msg',
427
+ timeout: 300 // 5 minutes in seconds
428
+ },
429
+ result_name: 'response',
430
+ categories: []
431
+ },
432
+ exits: []
433
+ };
434
+ const nodeUI = { type: 'wait_for_response' };
435
+ const el = (await fixture(html `
436
+ <temba-node-editor
437
+ .node=${node}
438
+ .nodeUI=${nodeUI}
439
+ .isOpen=${true}
440
+ ></temba-node-editor>
441
+ `));
442
+ await el.updateComplete;
443
+ // Wait for form data initialization
444
+ await new Promise((resolve) => setTimeout(resolve, 200));
445
+ await el.updateComplete;
446
+ // Check that the dialog is rendered with correct header
447
+ const dialog = el.shadowRoot.querySelector('temba-dialog');
448
+ expect(dialog).to.not.be.null;
449
+ expect(dialog.getAttribute('header')).to.equal('Wait for Response');
450
+ // Check that timeout and result name fields are rendered
451
+ const textComponents = el.shadowRoot.querySelectorAll('temba-textinput');
452
+ expect(textComponents.length).to.equal(1);
453
+ // Verify the field labels
454
+ const labels = Array.from(textComponents).map((comp) => comp.getAttribute('label'));
455
+ expect(labels).to.include('Result Name');
456
+ });
457
+ it('prioritizes node config over action config for non-execute_actions nodes', async () => {
458
+ // Create a split_by_llm_categorize node that has both actions and should use node config
459
+ const node = {
460
+ uuid: 'test-node-uuid',
461
+ actions: [
462
+ {
463
+ uuid: 'call-llm-uuid',
464
+ type: 'call_llm',
465
+ llm: { uuid: 'llm-123', name: 'Test LLM' },
466
+ input: '@input',
467
+ instructions: '@(prompt("categorize", slice(node.categories, 0, -2)))',
468
+ output_local: '_llm_output'
469
+ }
470
+ ],
471
+ router: {
472
+ type: 'switch',
473
+ operand: '@locals._llm_output',
474
+ result_name: 'Intent',
475
+ categories: [
476
+ { uuid: 'cat-1', name: 'Greeting', exit_uuid: 'exit-1' },
477
+ { uuid: 'cat-2', name: 'Question', exit_uuid: 'exit-2' }
478
+ ]
479
+ },
480
+ exits: [
481
+ { uuid: 'exit-1', destination_uuid: null },
482
+ { uuid: 'exit-2', destination_uuid: null }
483
+ ]
484
+ };
485
+ const nodeUI = { type: 'split_by_llm_categorize' };
486
+ // Simulate having both node and action set (which happens when editing from flow)
487
+ const el = (await fixture(html `
488
+ <temba-node-editor
489
+ .node=${node}
490
+ .nodeUI=${nodeUI}
491
+ .action=${node.actions[0]}
492
+ .isOpen=${true}
493
+ >
494
+ </temba-node-editor>
495
+ `));
496
+ await el.updateComplete;
497
+ // Wait for form data initialization
498
+ await new Promise((resolve) => setTimeout(resolve, 200));
499
+ await el.updateComplete;
500
+ // Should show node editor (Split by AI Categorize), not action editor (Call LLM)
501
+ const dialog = el.shadowRoot.querySelector('temba-dialog');
502
+ expect(dialog.getAttribute('header')).to.equal('Split by AI');
503
+ // Should have node config fields (LLM, Input, Categories, Result Name)
504
+ const selectComponents = el.shadowRoot.querySelectorAll('temba-select');
505
+ const arrayComponents = el.shadowRoot.querySelectorAll('temba-array-editor');
506
+ // Should have LLM select and categories array (node config fields)
507
+ expect(selectComponents.length).to.equal(1);
508
+ expect(arrayComponents.length).to.equal(1);
509
+ });
510
+ it('initializes categories correctly for split_by_llm_categorize', async () => {
511
+ var _a;
512
+ const node = {
513
+ uuid: 'test-node-uuid',
514
+ actions: [
515
+ {
516
+ uuid: 'call-llm-uuid',
517
+ type: 'call_llm',
518
+ llm: { uuid: 'llm-123', name: 'Test LLM' },
519
+ input: '@input',
520
+ instructions: '@(prompt("categorize", slice(node.categories, 0, -2)))',
521
+ output_local: '_llm_output'
522
+ }
523
+ ],
524
+ router: {
525
+ type: 'switch',
526
+ operand: '@locals._llm_output',
527
+ result_name: 'Intent',
528
+ categories: [
529
+ { uuid: 'cat-1', name: 'Greeting', exit_uuid: 'exit-1' },
530
+ { uuid: 'cat-2', name: 'Question', exit_uuid: 'exit-2' },
531
+ { uuid: 'cat-3', name: 'Other', exit_uuid: 'exit-3' },
532
+ { uuid: 'cat-4', name: 'Failure', exit_uuid: 'exit-4' }
533
+ ]
534
+ },
535
+ exits: [
536
+ { uuid: 'exit-1', destination_uuid: null },
537
+ { uuid: 'exit-2', destination_uuid: null },
538
+ { uuid: 'exit-3', destination_uuid: null },
539
+ { uuid: 'exit-4', destination_uuid: null }
540
+ ]
541
+ };
542
+ const nodeUI = { type: 'split_by_llm_categorize' };
543
+ const el = (await fixture(html `
544
+ <temba-node-editor
545
+ .node=${node}
546
+ .nodeUI=${nodeUI}
547
+ .isOpen=${true}
548
+ ></temba-node-editor>
549
+ `));
550
+ await el.updateComplete;
551
+ // Wait for form data initialization
552
+ await new Promise((resolve) => setTimeout(resolve, 200));
553
+ await el.updateComplete;
554
+ // Access the component's formData directly to check initialization
555
+ const formData = el.formData;
556
+ // Should have 2 categories (Greeting and Question, excluding Other and Failure)
557
+ expect(formData.categories).to.be.an('array');
558
+ expect(formData.categories.length).to.equal(2);
559
+ expect(formData.categories[0].name).to.equal('Greeting');
560
+ expect(formData.categories[1].name).to.equal('Question');
561
+ // Check that the array editor component receives the correct value
562
+ const arrayEditor = el.shadowRoot.querySelector('temba-array-editor');
563
+ expect(arrayEditor).to.not.be.null;
564
+ // Wait a bit more for the array editor to fully render
565
+ await new Promise((resolve) => setTimeout(resolve, 500));
566
+ await el.updateComplete;
567
+ // Check the values of the textinput components within the array items
568
+ const textInputs = (_a = arrayEditor.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelectorAll('temba-textinput');
569
+ if (textInputs && textInputs.length >= 2) {
570
+ // The first two textinputs should have the category names
571
+ expect(textInputs[0].value).to.equal('Greeting');
572
+ expect(textInputs[1].value).to.equal('Question');
573
+ }
574
+ });
575
+ it('properly initializes categories when node is set after component creation', async () => {
576
+ var _a;
577
+ // First create the component without any data
578
+ const el = (await fixture(html `
579
+ <temba-node-editor .isOpen=${false}></temba-node-editor>
580
+ `));
581
+ await el.updateComplete;
582
+ // Then set the node data (simulating real usage)
583
+ const node = {
584
+ uuid: 'test-node-uuid',
585
+ actions: [
586
+ {
587
+ uuid: 'call-llm-uuid',
588
+ type: 'call_llm',
589
+ llm: { uuid: 'llm-123', name: 'Test LLM' },
590
+ input: '@input',
591
+ instructions: '@(prompt("categorize", slice(node.categories, 0, -2)))',
592
+ output_local: '_llm_output'
593
+ }
594
+ ],
595
+ router: {
596
+ type: 'switch',
597
+ operand: '@locals._llm_output',
598
+ result_name: 'Intent',
599
+ categories: [
600
+ { uuid: 'cat-1', name: 'Greeting', exit_uuid: 'exit-1' },
601
+ { uuid: 'cat-2', name: 'Question', exit_uuid: 'exit-2' },
602
+ { uuid: 'cat-3', name: 'Other', exit_uuid: 'exit-3' },
603
+ { uuid: 'cat-4', name: 'Failure', exit_uuid: 'exit-4' }
604
+ ]
605
+ },
606
+ exits: [
607
+ { uuid: 'exit-1', destination_uuid: null },
608
+ { uuid: 'exit-2', destination_uuid: null },
609
+ { uuid: 'exit-3', destination_uuid: null },
610
+ { uuid: 'exit-4', destination_uuid: null }
611
+ ]
612
+ };
613
+ const nodeUI = { type: 'split_by_llm_categorize' };
614
+ // Set the properties (this should trigger updated() and openDialog())
615
+ el.node = node;
616
+ el.nodeUI = nodeUI;
617
+ await el.updateComplete;
618
+ // Wait for dialog to open and form data to initialize
619
+ await new Promise((resolve) => setTimeout(resolve, 300));
620
+ await el.updateComplete;
621
+ // Check that the form data is properly initialized
622
+ const formData = el.formData;
623
+ expect(formData.categories).to.be.an('array');
624
+ expect(formData.categories.length).to.equal(2);
625
+ expect(formData.categories[0].name).to.equal('Greeting');
626
+ expect(formData.categories[1].name).to.equal('Question');
627
+ // Check that array editor gets the correct values
628
+ const arrayEditor = el.shadowRoot.querySelector('temba-array-editor');
629
+ expect(arrayEditor).to.not.be.null;
630
+ const textInputs = (_a = arrayEditor.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelectorAll('temba-textinput');
631
+ if (textInputs && textInputs.length >= 2) {
632
+ expect(textInputs[0].value).to.equal('Greeting');
633
+ expect(textInputs[1].value).to.equal('Question');
634
+ }
635
+ });
636
+ it('preserves UUIDs for unchanged categories in split_by_llm_categorize', async () => {
637
+ const originalNode = {
638
+ uuid: 'test-node-uuid',
639
+ actions: [
640
+ {
641
+ uuid: 'existing-call-llm-uuid',
642
+ type: 'call_llm',
643
+ llm: { uuid: 'llm-123', name: 'Test LLM' },
644
+ input: '@input',
645
+ instructions: '@(prompt("categorize", slice(node.categories, 0, -2)))',
646
+ output_local: '_llm_output'
647
+ }
648
+ ],
649
+ router: {
650
+ type: 'switch',
651
+ operand: '@locals._llm_output',
652
+ result_name: 'Intent',
653
+ categories: [
654
+ {
655
+ uuid: 'existing-cat-1',
656
+ name: 'Greeting',
657
+ exit_uuid: 'existing-exit-1'
658
+ },
659
+ {
660
+ uuid: 'existing-cat-2',
661
+ name: 'Question',
662
+ exit_uuid: 'existing-exit-2'
663
+ },
664
+ {
665
+ uuid: 'existing-cat-other',
666
+ name: 'Other',
667
+ exit_uuid: 'existing-exit-other'
668
+ },
669
+ {
670
+ uuid: 'existing-cat-failure',
671
+ name: 'Failure',
672
+ exit_uuid: 'existing-exit-failure'
673
+ }
674
+ ],
675
+ cases: [
676
+ {
677
+ uuid: 'existing-case-1',
678
+ type: 'has_only_text',
679
+ arguments: ['Greeting'],
680
+ category_uuid: 'existing-cat-1'
681
+ },
682
+ {
683
+ uuid: 'existing-case-2',
684
+ type: 'has_only_text',
685
+ arguments: ['Question'],
686
+ category_uuid: 'existing-cat-2'
687
+ },
688
+ {
689
+ uuid: 'existing-case-error',
690
+ type: 'has_only_text',
691
+ arguments: ['<ERROR>'],
692
+ category_uuid: 'existing-cat-failure'
693
+ }
694
+ ]
695
+ },
696
+ exits: [
697
+ { uuid: 'existing-exit-1', destination_uuid: 'some-destination-1' },
698
+ { uuid: 'existing-exit-2', destination_uuid: 'some-destination-2' },
699
+ { uuid: 'existing-exit-other', destination_uuid: null },
700
+ { uuid: 'existing-exit-failure', destination_uuid: null }
701
+ ]
702
+ };
703
+ // Import the node config to test fromFormData directly
704
+ const { split_by_llm_categorize } = await import('../src/flow/nodes/split_by_llm_categorize');
705
+ // Test with same categories - should preserve UUIDs
706
+ const formDataSame = {
707
+ llm: [{ value: 'llm-123', name: 'Test LLM' }],
708
+ input: '@input',
709
+ categories: [{ name: 'Greeting' }, { name: 'Question' }],
710
+ result_name: 'Intent'
711
+ };
712
+ const resultSame = split_by_llm_categorize.fromFormData(formDataSame, originalNode);
713
+ // Should preserve existing UUIDs for unchanged categories
714
+ expect(resultSame.actions[0].uuid).to.equal('existing-call-llm-uuid');
715
+ const greetingCategory = resultSame.router.categories.find((cat) => cat.name === 'Greeting');
716
+ const questionCategory = resultSame.router.categories.find((cat) => cat.name === 'Question');
717
+ const otherCategory = resultSame.router.categories.find((cat) => cat.name === 'Other');
718
+ const failureCategory = resultSame.router.categories.find((cat) => cat.name === 'Failure');
719
+ expect(greetingCategory.uuid).to.equal('existing-cat-1');
720
+ expect(greetingCategory.exit_uuid).to.equal('existing-exit-1');
721
+ expect(questionCategory.uuid).to.equal('existing-cat-2');
722
+ expect(questionCategory.exit_uuid).to.equal('existing-exit-2');
723
+ expect(otherCategory.uuid).to.equal('existing-cat-other');
724
+ expect(failureCategory.uuid).to.equal('existing-cat-failure');
725
+ // Should preserve destination UUIDs for exits
726
+ const greetingExit = resultSame.exits.find((exit) => exit.uuid === 'existing-exit-1');
727
+ const questionExit = resultSame.exits.find((exit) => exit.uuid === 'existing-exit-2');
728
+ expect(greetingExit.destination_uuid).to.equal('some-destination-1');
729
+ expect(questionExit.destination_uuid).to.equal('some-destination-2');
730
+ // Test with changed categories - should generate new UUIDs for new categories
731
+ const formDataChanged = {
732
+ llm: [{ value: 'llm-123', name: 'Test LLM' }],
733
+ input: '@input',
734
+ categories: [
735
+ { name: 'Greeting' }, // unchanged - should keep UUID
736
+ { name: 'NewCategory' } // new - should get new UUID
737
+ ],
738
+ result_name: 'Intent'
739
+ };
740
+ const resultChanged = split_by_llm_categorize.fromFormData(formDataChanged, originalNode);
741
+ const greetingCategoryChanged = resultChanged.router.categories.find((cat) => cat.name === 'Greeting');
742
+ const newCategory = resultChanged.router.categories.find((cat) => cat.name === 'NewCategory');
743
+ // Greeting should keep its existing UUID
744
+ expect(greetingCategoryChanged.uuid).to.equal('existing-cat-1');
745
+ expect(greetingCategoryChanged.exit_uuid).to.equal('existing-exit-1');
746
+ // NewCategory should get a new UUID (not one of the existing ones)
747
+ expect(newCategory.uuid).to.not.equal('existing-cat-1');
748
+ expect(newCategory.uuid).to.not.equal('existing-cat-2');
749
+ expect(newCategory.uuid).to.not.equal('existing-cat-other');
750
+ expect(newCategory.uuid).to.not.equal('existing-cat-failure');
751
+ expect(newCategory.uuid).to.have.length.greaterThan(0);
752
+ });
282
753
  });
283
754
  //# sourceMappingURL=temba-node-editor.test.js.map