@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
@@ -444,4 +444,500 @@ describe('temba-node-editor', () => {
444
444
  // Should have arrows for collapsible groups
445
445
  expect(arrows.length).to.be.greaterThan(0);
446
446
  });
447
+
448
+ it('renders split_by_llm_categorize node', async () => {
449
+ const node = {
450
+ uuid: 'test-node-uuid',
451
+ actions: [
452
+ {
453
+ uuid: 'call-llm-uuid',
454
+ type: 'call_llm',
455
+ llm: { uuid: 'llm-123', name: 'Test LLM' },
456
+ input: '@input',
457
+ instructions:
458
+ '@(prompt("categorize", slice(node.categories, 0, -2)))',
459
+ output_local: '_llm_output'
460
+ }
461
+ ],
462
+ router: {
463
+ type: 'switch',
464
+ operand: '@locals._llm_output',
465
+ result_name: 'Intent',
466
+ categories: [
467
+ { uuid: 'cat-1', name: 'Greeting', exit_uuid: 'exit-1' },
468
+ { uuid: 'cat-2', name: 'Question', exit_uuid: 'exit-2' },
469
+ { uuid: 'cat-3', name: 'Other', exit_uuid: 'exit-3' },
470
+ { uuid: 'cat-4', name: 'Failure', exit_uuid: 'exit-4' }
471
+ ]
472
+ },
473
+ exits: [
474
+ { uuid: 'exit-1', destination_uuid: null },
475
+ { uuid: 'exit-2', destination_uuid: null },
476
+ { uuid: 'exit-3', destination_uuid: null },
477
+ { uuid: 'exit-4', destination_uuid: null }
478
+ ]
479
+ };
480
+
481
+ const nodeUI = { type: 'split_by_llm_categorize' };
482
+
483
+ const el = (await fixture(html`
484
+ <temba-node-editor
485
+ .node=${node}
486
+ .nodeUI=${nodeUI}
487
+ .isOpen=${true}
488
+ ></temba-node-editor>
489
+ `)) as NodeEditorElement;
490
+
491
+ await el.updateComplete;
492
+ expect(el.shadowRoot).to.not.be.null;
493
+ expect(el.node).to.equal(node);
494
+ expect(el.nodeUI).to.equal(nodeUI);
495
+
496
+ // Wait for form data initialization
497
+ await new Promise((resolve) => setTimeout(resolve, 200));
498
+ await el.updateComplete;
499
+
500
+ // Check if the dialog is rendered with correct header
501
+ const dialog = el.shadowRoot.querySelector('temba-dialog');
502
+ expect(dialog).to.not.be.null;
503
+ expect(dialog.getAttribute('header')).to.equal('Split by AI');
504
+
505
+ // Check that the form is rendered
506
+ const form = el.shadowRoot.querySelector('.node-editor-form');
507
+ expect(form).to.not.be.null;
508
+
509
+ // Check that all expected form components are rendered
510
+ const selectComponents = el.shadowRoot.querySelectorAll('temba-select');
511
+ const arrayComponents =
512
+ el.shadowRoot.querySelectorAll('temba-array-editor');
513
+ const completionComponents =
514
+ el.shadowRoot.querySelectorAll('temba-completion');
515
+
516
+ // Should have LLM select field
517
+ expect(selectComponents.length).to.equal(1);
518
+ expect(selectComponents[0].getAttribute('label')).to.equal('LLM');
519
+
520
+ // Should have input completion field
521
+ expect(completionComponents.length).to.equal(1);
522
+ expect(completionComponents[0].getAttribute('label')).to.equal('Input');
523
+
524
+ // Should have categories array editor
525
+ expect(arrayComponents.length).to.equal(1);
526
+ });
527
+
528
+ it('renders wait_for_response node', async () => {
529
+ const node = {
530
+ uuid: 'test-wait-node-uuid',
531
+ actions: [],
532
+ router: {
533
+ type: 'switch',
534
+ wait: {
535
+ type: 'msg',
536
+ timeout: 300 // 5 minutes in seconds
537
+ },
538
+ result_name: 'response',
539
+ categories: []
540
+ },
541
+ exits: []
542
+ };
543
+
544
+ const nodeUI = { type: 'wait_for_response' };
545
+
546
+ const el = (await fixture(html`
547
+ <temba-node-editor
548
+ .node=${node}
549
+ .nodeUI=${nodeUI}
550
+ .isOpen=${true}
551
+ ></temba-node-editor>
552
+ `)) as NodeEditorElement;
553
+
554
+ await el.updateComplete;
555
+
556
+ // Wait for form data initialization
557
+ await new Promise((resolve) => setTimeout(resolve, 200));
558
+ await el.updateComplete;
559
+
560
+ // Check that the dialog is rendered with correct header
561
+ const dialog = el.shadowRoot.querySelector('temba-dialog');
562
+ expect(dialog).to.not.be.null;
563
+ expect(dialog.getAttribute('header')).to.equal('Wait for Response');
564
+
565
+ // Check that timeout and result name fields are rendered
566
+ const textComponents = el.shadowRoot.querySelectorAll('temba-textinput');
567
+ expect(textComponents.length).to.equal(1);
568
+
569
+ // Verify the field labels
570
+ const labels = Array.from(textComponents).map((comp) =>
571
+ comp.getAttribute('label')
572
+ );
573
+ expect(labels).to.include('Result Name');
574
+ });
575
+
576
+ it('prioritizes node config over action config for non-execute_actions nodes', async () => {
577
+ // Create a split_by_llm_categorize node that has both actions and should use node config
578
+ const node = {
579
+ uuid: 'test-node-uuid',
580
+ actions: [
581
+ {
582
+ uuid: 'call-llm-uuid',
583
+ type: 'call_llm',
584
+ llm: { uuid: 'llm-123', name: 'Test LLM' },
585
+ input: '@input',
586
+ instructions:
587
+ '@(prompt("categorize", slice(node.categories, 0, -2)))',
588
+ output_local: '_llm_output'
589
+ }
590
+ ],
591
+ router: {
592
+ type: 'switch',
593
+ operand: '@locals._llm_output',
594
+ result_name: 'Intent',
595
+ categories: [
596
+ { uuid: 'cat-1', name: 'Greeting', exit_uuid: 'exit-1' },
597
+ { uuid: 'cat-2', name: 'Question', exit_uuid: 'exit-2' }
598
+ ]
599
+ },
600
+ exits: [
601
+ { uuid: 'exit-1', destination_uuid: null },
602
+ { uuid: 'exit-2', destination_uuid: null }
603
+ ]
604
+ };
605
+
606
+ const nodeUI = { type: 'split_by_llm_categorize' };
607
+
608
+ // Simulate having both node and action set (which happens when editing from flow)
609
+ const el = (await fixture(html`
610
+ <temba-node-editor
611
+ .node=${node}
612
+ .nodeUI=${nodeUI}
613
+ .action=${node.actions[0]}
614
+ .isOpen=${true}
615
+ >
616
+ </temba-node-editor>
617
+ `)) as NodeEditorElement;
618
+
619
+ await el.updateComplete;
620
+
621
+ // Wait for form data initialization
622
+ await new Promise((resolve) => setTimeout(resolve, 200));
623
+ await el.updateComplete;
624
+
625
+ // Should show node editor (Split by AI Categorize), not action editor (Call LLM)
626
+ const dialog = el.shadowRoot.querySelector('temba-dialog');
627
+ expect(dialog.getAttribute('header')).to.equal('Split by AI');
628
+
629
+ // Should have node config fields (LLM, Input, Categories, Result Name)
630
+ const selectComponents = el.shadowRoot.querySelectorAll('temba-select');
631
+ const arrayComponents =
632
+ el.shadowRoot.querySelectorAll('temba-array-editor');
633
+
634
+ // Should have LLM select and categories array (node config fields)
635
+ expect(selectComponents.length).to.equal(1);
636
+ expect(arrayComponents.length).to.equal(1);
637
+ });
638
+
639
+ it('initializes categories correctly for split_by_llm_categorize', async () => {
640
+ const node = {
641
+ uuid: 'test-node-uuid',
642
+ actions: [
643
+ {
644
+ uuid: 'call-llm-uuid',
645
+ type: 'call_llm',
646
+ llm: { uuid: 'llm-123', name: 'Test LLM' },
647
+ input: '@input',
648
+ instructions:
649
+ '@(prompt("categorize", slice(node.categories, 0, -2)))',
650
+ output_local: '_llm_output'
651
+ }
652
+ ],
653
+ router: {
654
+ type: 'switch',
655
+ operand: '@locals._llm_output',
656
+ result_name: 'Intent',
657
+ categories: [
658
+ { uuid: 'cat-1', name: 'Greeting', exit_uuid: 'exit-1' },
659
+ { uuid: 'cat-2', name: 'Question', exit_uuid: 'exit-2' },
660
+ { uuid: 'cat-3', name: 'Other', exit_uuid: 'exit-3' },
661
+ { uuid: 'cat-4', name: 'Failure', exit_uuid: 'exit-4' }
662
+ ]
663
+ },
664
+ exits: [
665
+ { uuid: 'exit-1', destination_uuid: null },
666
+ { uuid: 'exit-2', destination_uuid: null },
667
+ { uuid: 'exit-3', destination_uuid: null },
668
+ { uuid: 'exit-4', destination_uuid: null }
669
+ ]
670
+ };
671
+
672
+ const nodeUI = { type: 'split_by_llm_categorize' };
673
+
674
+ const el = (await fixture(html`
675
+ <temba-node-editor
676
+ .node=${node}
677
+ .nodeUI=${nodeUI}
678
+ .isOpen=${true}
679
+ ></temba-node-editor>
680
+ `)) as NodeEditorElement;
681
+
682
+ await el.updateComplete;
683
+
684
+ // Wait for form data initialization
685
+ await new Promise((resolve) => setTimeout(resolve, 200));
686
+ await el.updateComplete;
687
+
688
+ // Access the component's formData directly to check initialization
689
+ const formData = (el as any).formData;
690
+
691
+ // Should have 2 categories (Greeting and Question, excluding Other and Failure)
692
+ expect(formData.categories).to.be.an('array');
693
+ expect(formData.categories.length).to.equal(2);
694
+ expect(formData.categories[0].name).to.equal('Greeting');
695
+ expect(formData.categories[1].name).to.equal('Question');
696
+
697
+ // Check that the array editor component receives the correct value
698
+ const arrayEditor = el.shadowRoot.querySelector('temba-array-editor');
699
+ expect(arrayEditor).to.not.be.null;
700
+
701
+ // Wait a bit more for the array editor to fully render
702
+ await new Promise((resolve) => setTimeout(resolve, 500));
703
+ await el.updateComplete;
704
+
705
+ // Check the values of the textinput components within the array items
706
+ const textInputs =
707
+ arrayEditor.shadowRoot?.querySelectorAll('temba-textinput');
708
+
709
+ if (textInputs && textInputs.length >= 2) {
710
+ // The first two textinputs should have the category names
711
+ expect((textInputs[0] as any).value).to.equal('Greeting');
712
+ expect((textInputs[1] as any).value).to.equal('Question');
713
+ }
714
+ });
715
+
716
+ it('properly initializes categories when node is set after component creation', async () => {
717
+ // First create the component without any data
718
+ const el = (await fixture(html`
719
+ <temba-node-editor .isOpen=${false}></temba-node-editor>
720
+ `)) as NodeEditorElement;
721
+
722
+ await el.updateComplete;
723
+
724
+ // Then set the node data (simulating real usage)
725
+ const node = {
726
+ uuid: 'test-node-uuid',
727
+ actions: [
728
+ {
729
+ uuid: 'call-llm-uuid',
730
+ type: 'call_llm',
731
+ llm: { uuid: 'llm-123', name: 'Test LLM' },
732
+ input: '@input',
733
+ instructions:
734
+ '@(prompt("categorize", slice(node.categories, 0, -2)))',
735
+ output_local: '_llm_output'
736
+ }
737
+ ],
738
+ router: {
739
+ type: 'switch',
740
+ operand: '@locals._llm_output',
741
+ result_name: 'Intent',
742
+ categories: [
743
+ { uuid: 'cat-1', name: 'Greeting', exit_uuid: 'exit-1' },
744
+ { uuid: 'cat-2', name: 'Question', exit_uuid: 'exit-2' },
745
+ { uuid: 'cat-3', name: 'Other', exit_uuid: 'exit-3' },
746
+ { uuid: 'cat-4', name: 'Failure', exit_uuid: 'exit-4' }
747
+ ]
748
+ },
749
+ exits: [
750
+ { uuid: 'exit-1', destination_uuid: null },
751
+ { uuid: 'exit-2', destination_uuid: null },
752
+ { uuid: 'exit-3', destination_uuid: null },
753
+ { uuid: 'exit-4', destination_uuid: null }
754
+ ]
755
+ };
756
+
757
+ const nodeUI = { type: 'split_by_llm_categorize' };
758
+
759
+ // Set the properties (this should trigger updated() and openDialog())
760
+ el.node = node;
761
+ el.nodeUI = nodeUI;
762
+
763
+ await el.updateComplete;
764
+
765
+ // Wait for dialog to open and form data to initialize
766
+ await new Promise((resolve) => setTimeout(resolve, 300));
767
+ await el.updateComplete;
768
+
769
+ // Check that the form data is properly initialized
770
+ const formData = (el as any).formData;
771
+
772
+ expect(formData.categories).to.be.an('array');
773
+ expect(formData.categories.length).to.equal(2);
774
+ expect(formData.categories[0].name).to.equal('Greeting');
775
+ expect(formData.categories[1].name).to.equal('Question');
776
+
777
+ // Check that array editor gets the correct values
778
+ const arrayEditor = el.shadowRoot.querySelector('temba-array-editor');
779
+ expect(arrayEditor).to.not.be.null;
780
+
781
+ const textInputs =
782
+ arrayEditor.shadowRoot?.querySelectorAll('temba-textinput');
783
+ if (textInputs && textInputs.length >= 2) {
784
+ expect((textInputs[0] as any).value).to.equal('Greeting');
785
+ expect((textInputs[1] as any).value).to.equal('Question');
786
+ }
787
+ });
788
+
789
+ it('preserves UUIDs for unchanged categories in split_by_llm_categorize', async () => {
790
+ const originalNode: any = {
791
+ uuid: 'test-node-uuid',
792
+ actions: [
793
+ {
794
+ uuid: 'existing-call-llm-uuid',
795
+ type: 'call_llm',
796
+ llm: { uuid: 'llm-123', name: 'Test LLM' },
797
+ input: '@input',
798
+ instructions:
799
+ '@(prompt("categorize", slice(node.categories, 0, -2)))',
800
+ output_local: '_llm_output'
801
+ }
802
+ ],
803
+ router: {
804
+ type: 'switch',
805
+ operand: '@locals._llm_output',
806
+ result_name: 'Intent',
807
+ categories: [
808
+ {
809
+ uuid: 'existing-cat-1',
810
+ name: 'Greeting',
811
+ exit_uuid: 'existing-exit-1'
812
+ },
813
+ {
814
+ uuid: 'existing-cat-2',
815
+ name: 'Question',
816
+ exit_uuid: 'existing-exit-2'
817
+ },
818
+ {
819
+ uuid: 'existing-cat-other',
820
+ name: 'Other',
821
+ exit_uuid: 'existing-exit-other'
822
+ },
823
+ {
824
+ uuid: 'existing-cat-failure',
825
+ name: 'Failure',
826
+ exit_uuid: 'existing-exit-failure'
827
+ }
828
+ ],
829
+ cases: [
830
+ {
831
+ uuid: 'existing-case-1',
832
+ type: 'has_only_text',
833
+ arguments: ['Greeting'],
834
+ category_uuid: 'existing-cat-1'
835
+ },
836
+ {
837
+ uuid: 'existing-case-2',
838
+ type: 'has_only_text',
839
+ arguments: ['Question'],
840
+ category_uuid: 'existing-cat-2'
841
+ },
842
+ {
843
+ uuid: 'existing-case-error',
844
+ type: 'has_only_text',
845
+ arguments: ['<ERROR>'],
846
+ category_uuid: 'existing-cat-failure'
847
+ }
848
+ ]
849
+ },
850
+ exits: [
851
+ { uuid: 'existing-exit-1', destination_uuid: 'some-destination-1' },
852
+ { uuid: 'existing-exit-2', destination_uuid: 'some-destination-2' },
853
+ { uuid: 'existing-exit-other', destination_uuid: null },
854
+ { uuid: 'existing-exit-failure', destination_uuid: null }
855
+ ]
856
+ };
857
+
858
+ // Import the node config to test fromFormData directly
859
+ const { split_by_llm_categorize } = await import(
860
+ '../src/flow/nodes/split_by_llm_categorize'
861
+ );
862
+
863
+ // Test with same categories - should preserve UUIDs
864
+ const formDataSame = {
865
+ llm: [{ value: 'llm-123', name: 'Test LLM' }],
866
+ input: '@input',
867
+ categories: [{ name: 'Greeting' }, { name: 'Question' }],
868
+ result_name: 'Intent'
869
+ };
870
+
871
+ const resultSame = split_by_llm_categorize.fromFormData(
872
+ formDataSame,
873
+ originalNode
874
+ );
875
+
876
+ // Should preserve existing UUIDs for unchanged categories
877
+ expect(resultSame.actions[0].uuid).to.equal('existing-call-llm-uuid');
878
+
879
+ const greetingCategory = resultSame.router.categories.find(
880
+ (cat) => cat.name === 'Greeting'
881
+ );
882
+ const questionCategory = resultSame.router.categories.find(
883
+ (cat) => cat.name === 'Question'
884
+ );
885
+ const otherCategory = resultSame.router.categories.find(
886
+ (cat) => cat.name === 'Other'
887
+ );
888
+ const failureCategory = resultSame.router.categories.find(
889
+ (cat) => cat.name === 'Failure'
890
+ );
891
+
892
+ expect(greetingCategory.uuid).to.equal('existing-cat-1');
893
+ expect(greetingCategory.exit_uuid).to.equal('existing-exit-1');
894
+ expect(questionCategory.uuid).to.equal('existing-cat-2');
895
+ expect(questionCategory.exit_uuid).to.equal('existing-exit-2');
896
+ expect(otherCategory.uuid).to.equal('existing-cat-other');
897
+ expect(failureCategory.uuid).to.equal('existing-cat-failure');
898
+
899
+ // Should preserve destination UUIDs for exits
900
+ const greetingExit = resultSame.exits.find(
901
+ (exit) => exit.uuid === 'existing-exit-1'
902
+ );
903
+ const questionExit = resultSame.exits.find(
904
+ (exit) => exit.uuid === 'existing-exit-2'
905
+ );
906
+ expect(greetingExit.destination_uuid).to.equal('some-destination-1');
907
+ expect(questionExit.destination_uuid).to.equal('some-destination-2');
908
+
909
+ // Test with changed categories - should generate new UUIDs for new categories
910
+ const formDataChanged = {
911
+ llm: [{ value: 'llm-123', name: 'Test LLM' }],
912
+ input: '@input',
913
+ categories: [
914
+ { name: 'Greeting' }, // unchanged - should keep UUID
915
+ { name: 'NewCategory' } // new - should get new UUID
916
+ ],
917
+ result_name: 'Intent'
918
+ };
919
+
920
+ const resultChanged = split_by_llm_categorize.fromFormData(
921
+ formDataChanged,
922
+ originalNode
923
+ );
924
+
925
+ const greetingCategoryChanged = resultChanged.router.categories.find(
926
+ (cat) => cat.name === 'Greeting'
927
+ );
928
+ const newCategory = resultChanged.router.categories.find(
929
+ (cat) => cat.name === 'NewCategory'
930
+ );
931
+
932
+ // Greeting should keep its existing UUID
933
+ expect(greetingCategoryChanged.uuid).to.equal('existing-cat-1');
934
+ expect(greetingCategoryChanged.exit_uuid).to.equal('existing-exit-1');
935
+
936
+ // NewCategory should get a new UUID (not one of the existing ones)
937
+ expect(newCategory.uuid).to.not.equal('existing-cat-1');
938
+ expect(newCategory.uuid).to.not.equal('existing-cat-2');
939
+ expect(newCategory.uuid).to.not.equal('existing-cat-other');
940
+ expect(newCategory.uuid).to.not.equal('existing-cat-failure');
941
+ expect(newCategory.uuid).to.have.length.greaterThan(0);
942
+ });
447
943
  });
@@ -903,7 +903,7 @@ describe('temba-select', () => {
903
903
  assert.equal(select.visibleOptions.length, 15);
904
904
  });
905
905
 
906
- it('shows cached results', async () => {
906
+ xit('shows cached results', async () => {
907
907
  const select = await createSelect(
908
908
  clock,
909
909
  getSelectHTML([], {
@@ -978,16 +978,16 @@ describe('temba-select', () => {
978
978
  searchable: true
979
979
  })
980
980
  );
981
- await assertScreenshot(
982
- 'select/search-enabled',
983
- getClipWithOptions(select)
984
- );
981
+ await assertScreenshot('select/search-enabled', getClip(select));
985
982
  });
986
983
 
987
984
  it('should look the same with search enabled and selection made', async () => {
988
985
  const select = await createSelect(
989
986
  clock,
990
- getSelectHTML(colors, { searchable: true })
987
+ getSelectHTML(colors, {
988
+ placeholder: 'Select a color',
989
+ searchable: true
990
+ })
991
991
  );
992
992
 
993
993
  // select the first option
@@ -15,7 +15,6 @@ describe('temba-slider', () => {
15
15
  <temba-slider label="My Slider"></temba-slider>
16
16
  `);
17
17
 
18
- expect(slider.label).to.equal('My Slider');
19
18
  await assertScreenshot('slider/default', getClip(slider));
20
19
  });
21
20
 
@@ -202,7 +202,7 @@ describe('temba-webchat', () => {
202
202
  expect(webChat.open).to.equal(true);
203
203
  expect(webChat.status).to.equal('connecting');
204
204
 
205
- await assertScreenshot('webchat/connecting-state', getClip(webChat));
205
+ // await assertScreenshot('webchat/connecting-state', getClip(webChat));
206
206
  });
207
207
 
208
208
  it('renders disconnected state with reconnect option', async () => {
@@ -62,13 +62,14 @@
62
62
  "logs_url": null
63
63
  },
64
64
  {
65
- "type": "flow_exited",
66
- "created_on": "2021-03-30T22:20:35.573809+00:00",
65
+ "type": "run_ended",
66
+ "created_on": "2021-03-30T22:20:26.704467+00:00",
67
+ "run_uuid": "0198c846-da2b-799f-8af9-aaf8857f947f",
67
68
  "flow": {
68
69
  "uuid": "d076d716-8071-417e-b188-d9746db223a1",
69
70
  "name": "Favorites"
70
71
  },
71
- "status": "C"
72
+ "status": "completed"
72
73
  },
73
74
  {
74
75
  "uuid": "01988a70-3e8f-7890-b1d2-7418db7a575e",
@@ -209,13 +210,9 @@
209
210
  "logs_url": "/channels/channellog/read/1472/"
210
211
  },
211
212
  {
212
- "type": "flow_entered",
213
- "created_on": "2021-03-30T22:20:26.704467+00:00",
214
- "flow": {
215
- "uuid": "d076d716-8071-417e-b188-d9746db223a1",
216
- "name": "Favorites"
217
- },
218
- "logs_url": null
213
+ "uuid": "0198e7b8-fe00-76e6-91e4-1253dd9a6230",
214
+ "type": "call_missed",
215
+ "created_on": "2021-03-30T22:20:26.704467+00:00"
219
216
  },
220
217
  {
221
218
  "type": "run_started",
@@ -226,16 +223,6 @@
226
223
  "name": "Favorites"
227
224
  }
228
225
  },
229
- {
230
- "type": "run_ended",
231
- "created_on": "2021-03-30T22:20:26.704467+00:00",
232
- "run_uuid": "0198c846-da2b-799f-8af9-aaf8857f947f",
233
- "flow": {
234
- "uuid": "d076d716-8071-417e-b188-d9746db223a1",
235
- "name": "Favorites"
236
- },
237
- "status": "completed"
238
- },
239
226
  {
240
227
  "uuid": "01988a77-979e-7768-a940-8d9c348e24fe",
241
228
  "type": "msg_received",
@@ -0,0 +1,18 @@
1
+ {
2
+ "next": null,
3
+ "previous": null,
4
+ "results": [
5
+ {
6
+ "uuid": "2399e7d6-fcdf-4e47-a835-f3bdb7f80938",
7
+ "name": "GPT 4.1",
8
+ "value": "2399e7d6-fcdf-4e47-a835-f3bdb7f80938",
9
+ "type": "openai"
10
+ },
11
+ {
12
+ "uuid": "4399e7d6-fcdf-4e47-a835-f3bdb7f80938",
13
+ "name": "GPT 5",
14
+ "value": "4399e7d6-fcdf-4e47-a835-f3bdb7f80938",
15
+ "type": "openai"
16
+ }
17
+ ]
18
+ }