@nyaruka/temba-components 0.129.8 → 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 (203) hide show
  1. package/CHANGELOG.md +27 -3
  2. package/demo/data/flows/sample-flow.json +186 -96
  3. package/dist/temba-components.js +414 -351
  4. package/dist/temba-components.js.map +1 -1
  5. package/out-tsc/src/events.js.map +1 -1
  6. package/out-tsc/src/excellent/helpers.js +2 -2
  7. package/out-tsc/src/excellent/helpers.js.map +1 -1
  8. package/out-tsc/src/flow/CanvasNode.js +25 -7
  9. package/out-tsc/src/flow/CanvasNode.js.map +1 -1
  10. package/out-tsc/src/flow/Editor.js +11 -1
  11. package/out-tsc/src/flow/Editor.js.map +1 -1
  12. package/out-tsc/src/flow/NodeEditor.js +133 -290
  13. package/out-tsc/src/flow/NodeEditor.js.map +1 -1
  14. package/out-tsc/src/flow/actions/add_input_labels.js +40 -0
  15. package/out-tsc/src/flow/actions/add_input_labels.js.map +1 -1
  16. package/out-tsc/src/flow/actions/call_llm.js +56 -3
  17. package/out-tsc/src/flow/actions/call_llm.js.map +1 -1
  18. package/out-tsc/src/flow/actions/call_webhook.js +1 -1
  19. package/out-tsc/src/flow/actions/call_webhook.js.map +1 -1
  20. package/out-tsc/src/flow/actions/open_ticket.js +65 -3
  21. package/out-tsc/src/flow/actions/open_ticket.js.map +1 -1
  22. package/out-tsc/src/flow/actions/set_run_result.js +75 -0
  23. package/out-tsc/src/flow/actions/set_run_result.js.map +1 -1
  24. package/out-tsc/src/flow/config.js +4 -0
  25. package/out-tsc/src/flow/config.js.map +1 -1
  26. package/out-tsc/src/flow/nodes/split_by_llm_categorize.js +227 -0
  27. package/out-tsc/src/flow/nodes/split_by_llm_categorize.js.map +1 -0
  28. package/out-tsc/src/flow/nodes/split_by_ticket.js +18 -0
  29. package/out-tsc/src/flow/nodes/split_by_ticket.js.map +1 -0
  30. package/out-tsc/src/flow/nodes/wait_for_response.js +27 -1
  31. package/out-tsc/src/flow/nodes/wait_for_response.js.map +1 -1
  32. package/out-tsc/src/flow/types.js +0 -65
  33. package/out-tsc/src/flow/types.js.map +1 -1
  34. package/out-tsc/src/form/ArrayEditor.js +18 -61
  35. package/out-tsc/src/form/ArrayEditor.js.map +1 -1
  36. package/out-tsc/src/form/FieldRenderer.js +305 -0
  37. package/out-tsc/src/form/FieldRenderer.js.map +1 -0
  38. package/out-tsc/src/form/FormField.js +3 -3
  39. package/out-tsc/src/form/FormField.js.map +1 -1
  40. package/out-tsc/src/form/TextInput.js +1 -1
  41. package/out-tsc/src/form/TextInput.js.map +1 -1
  42. package/out-tsc/src/form/select/Select.js +48 -20
  43. package/out-tsc/src/form/select/Select.js.map +1 -1
  44. package/out-tsc/src/live/ContactChat.js +39 -13
  45. package/out-tsc/src/live/ContactChat.js.map +1 -1
  46. package/out-tsc/src/markdown.js +13 -11
  47. package/out-tsc/src/markdown.js.map +1 -1
  48. package/out-tsc/test/ActionHelper.js +2 -0
  49. package/out-tsc/test/ActionHelper.js.map +1 -1
  50. package/out-tsc/test/NodeHelper.js +148 -0
  51. package/out-tsc/test/NodeHelper.js.map +1 -0
  52. package/out-tsc/test/actions/call_llm.test.js +103 -0
  53. package/out-tsc/test/actions/call_llm.test.js.map +1 -0
  54. package/out-tsc/test/nodes/split_by_llm_categorize.test.js +532 -0
  55. package/out-tsc/test/nodes/split_by_llm_categorize.test.js.map +1 -0
  56. package/out-tsc/test/nodes/split_by_random.test.js +150 -0
  57. package/out-tsc/test/nodes/split_by_random.test.js.map +1 -0
  58. package/out-tsc/test/nodes/wait_for_digits.test.js +150 -0
  59. package/out-tsc/test/nodes/wait_for_digits.test.js.map +1 -0
  60. package/out-tsc/test/nodes/wait_for_response.test.js +171 -0
  61. package/out-tsc/test/nodes/wait_for_response.test.js.map +1 -0
  62. package/out-tsc/test/temba-add-input-labels.test.js +70 -0
  63. package/out-tsc/test/temba-add-input-labels.test.js.map +1 -0
  64. package/out-tsc/test/temba-field-renderer.test.js +296 -0
  65. package/out-tsc/test/temba-field-renderer.test.js.map +1 -0
  66. package/out-tsc/test/temba-markdown.test.js +1 -1
  67. package/out-tsc/test/temba-markdown.test.js.map +1 -1
  68. package/out-tsc/test/temba-node-editor.test.js +400 -0
  69. package/out-tsc/test/temba-node-editor.test.js.map +1 -1
  70. package/out-tsc/test/temba-select.test.js +6 -3
  71. package/out-tsc/test/temba-select.test.js.map +1 -1
  72. package/out-tsc/test/temba-webchat.test.js +1 -1
  73. package/out-tsc/test/temba-webchat.test.js.map +1 -1
  74. package/package.json +1 -1
  75. package/screenshots/truth/actions/add_contact_groups/editor/descriptive-group-names.png +0 -0
  76. package/screenshots/truth/actions/add_contact_groups/editor/long-group-names.png +0 -0
  77. package/screenshots/truth/actions/add_contact_groups/editor/many-groups.png +0 -0
  78. package/screenshots/truth/actions/call_llm/editor/information-extraction.png +0 -0
  79. package/screenshots/truth/actions/call_llm/editor/sentiment-analysis.png +0 -0
  80. package/screenshots/truth/actions/call_llm/editor/summarization.png +0 -0
  81. package/screenshots/truth/actions/call_llm/editor/translation-task.png +0 -0
  82. package/screenshots/truth/actions/call_llm/render/information-extraction.png +0 -0
  83. package/screenshots/truth/actions/call_llm/render/sentiment-analysis.png +0 -0
  84. package/screenshots/truth/actions/call_llm/render/summarization.png +0 -0
  85. package/screenshots/truth/actions/call_llm/render/translation-task.png +0 -0
  86. package/screenshots/truth/actions/remove_contact_groups/editor/long-descriptive-group-names.png +0 -0
  87. package/screenshots/truth/actions/remove_contact_groups/editor/many-groups.png +0 -0
  88. package/screenshots/truth/actions/remove_contact_groups/editor/remove-from-all-groups.png +0 -0
  89. package/screenshots/truth/actions/send_email/editor/complex-business-email.png +0 -0
  90. package/screenshots/truth/actions/send_email/editor/multiple-recipients.png +0 -0
  91. package/screenshots/truth/actions/send_msg/editor/long-quick-replies.png +0 -0
  92. package/screenshots/truth/actions/send_msg/editor/multiline-text-with-replies.png +0 -0
  93. package/screenshots/truth/actions/send_msg/editor/simple-text.png +0 -0
  94. package/screenshots/truth/actions/send_msg/editor/text-with-linebreaks.png +0 -0
  95. package/screenshots/truth/actions/send_msg/editor/text-with-many-quick-replies.png +0 -0
  96. package/screenshots/truth/actions/send_msg/editor/text-with-quick-replies.png +0 -0
  97. package/screenshots/truth/actions/send_msg/editor/text-without-quick-replies.png +0 -0
  98. package/screenshots/truth/editor/router.png +0 -0
  99. package/screenshots/truth/editor/send_msg.png +0 -0
  100. package/screenshots/truth/editor/set_contact_language.png +0 -0
  101. package/screenshots/truth/editor/set_contact_name.png +0 -0
  102. package/screenshots/truth/editor/set_run_result.png +0 -0
  103. package/screenshots/truth/editor/wait.png +0 -0
  104. package/screenshots/truth/field-renderer/checkbox-checked.png +0 -0
  105. package/screenshots/truth/field-renderer/checkbox-unchecked.png +0 -0
  106. package/screenshots/truth/field-renderer/checkbox-with-errors.png +0 -0
  107. package/screenshots/truth/field-renderer/context-comparison.png +0 -0
  108. package/screenshots/truth/field-renderer/key-value-with-label.png +0 -0
  109. package/screenshots/truth/field-renderer/message-editor-with-label.png +0 -0
  110. package/screenshots/truth/field-renderer/select-multi.png +0 -0
  111. package/screenshots/truth/field-renderer/select-no-label.png +0 -0
  112. package/screenshots/truth/field-renderer/select-with-label.png +0 -0
  113. package/screenshots/truth/field-renderer/text-evaluated.png +0 -0
  114. package/screenshots/truth/field-renderer/text-no-label.png +0 -0
  115. package/screenshots/truth/field-renderer/text-with-errors.png +0 -0
  116. package/screenshots/truth/field-renderer/text-with-label.png +0 -0
  117. package/screenshots/truth/field-renderer/textarea-evaluated.png +0 -0
  118. package/screenshots/truth/field-renderer/textarea-with-label.png +0 -0
  119. package/screenshots/truth/nodes/split_by_llm_categorize/editor/basic-categorization.png +0 -0
  120. package/screenshots/truth/nodes/split_by_llm_categorize/editor/custom-input-and-result-name.png +0 -0
  121. package/screenshots/truth/nodes/split_by_llm_categorize/editor/feedback-categorization.png +0 -0
  122. package/screenshots/truth/nodes/split_by_llm_categorize/editor/many-categories.png +0 -0
  123. package/screenshots/truth/nodes/split_by_llm_categorize/editor/minimal-categories.png +0 -0
  124. package/screenshots/truth/nodes/split_by_llm_categorize/render/basic-categorization.png +0 -0
  125. package/screenshots/truth/nodes/split_by_llm_categorize/render/custom-input-and-result-name.png +0 -0
  126. package/screenshots/truth/nodes/split_by_llm_categorize/render/feedback-categorization.png +0 -0
  127. package/screenshots/truth/nodes/split_by_llm_categorize/render/many-categories.png +0 -0
  128. package/screenshots/truth/nodes/split_by_llm_categorize/render/minimal-categories.png +0 -0
  129. package/screenshots/truth/nodes/split_by_random/editor/ab-test-multiple-variants.png +0 -0
  130. package/screenshots/truth/nodes/split_by_random/editor/sampling-split.png +0 -0
  131. package/screenshots/truth/nodes/split_by_random/editor/three-way-split.png +0 -0
  132. package/screenshots/truth/nodes/split_by_random/editor/two-bucket-split.png +0 -0
  133. package/screenshots/truth/nodes/split_by_random/render/ab-test-multiple-variants.png +0 -0
  134. package/screenshots/truth/nodes/split_by_random/render/sampling-split.png +0 -0
  135. package/screenshots/truth/nodes/split_by_random/render/three-way-split.png +0 -0
  136. package/screenshots/truth/nodes/split_by_random/render/two-bucket-split.png +0 -0
  137. package/screenshots/truth/nodes/wait_for_digits/editor/basic-digits-wait.png +0 -0
  138. package/screenshots/truth/nodes/wait_for_digits/editor/phone-number-collection.png +0 -0
  139. package/screenshots/truth/nodes/wait_for_digits/editor/single-digit-with-timeout.png +0 -0
  140. package/screenshots/truth/nodes/wait_for_digits/editor/verification-code.png +0 -0
  141. package/screenshots/truth/nodes/wait_for_digits/render/basic-digits-wait.png +0 -0
  142. package/screenshots/truth/nodes/wait_for_digits/render/phone-number-collection.png +0 -0
  143. package/screenshots/truth/nodes/wait_for_digits/render/single-digit-with-timeout.png +0 -0
  144. package/screenshots/truth/nodes/wait_for_digits/render/verification-code.png +0 -0
  145. package/screenshots/truth/nodes/wait_for_response/editor/basic-wait.png +0 -0
  146. package/screenshots/truth/nodes/wait_for_response/editor/custom-result-name.png +0 -0
  147. package/screenshots/truth/nodes/wait_for_response/editor/no-timeout.png +0 -0
  148. package/screenshots/truth/nodes/wait_for_response/editor/short-timeout.png +0 -0
  149. package/screenshots/truth/nodes/wait_for_response/render/basic-wait.png +0 -0
  150. package/screenshots/truth/nodes/wait_for_response/render/custom-result-name.png +0 -0
  151. package/screenshots/truth/nodes/wait_for_response/render/no-timeout.png +0 -0
  152. package/screenshots/truth/nodes/wait_for_response/render/short-timeout.png +0 -0
  153. package/screenshots/truth/omnibox/selected.png +0 -0
  154. package/screenshots/truth/select/functions.png +0 -0
  155. package/screenshots/truth/select/multi-with-endpoint.png +0 -0
  156. package/screenshots/truth/select/search-enabled.png +0 -0
  157. package/src/events.ts +8 -1
  158. package/src/excellent/helpers.ts +2 -2
  159. package/src/flow/CanvasNode.ts +22 -1
  160. package/src/flow/Editor.ts +12 -1
  161. package/src/flow/NodeEditor.ts +186 -374
  162. package/src/flow/actions/add_input_labels.ts +45 -0
  163. package/src/flow/actions/call_llm.ts +57 -3
  164. package/src/flow/actions/call_webhook.ts +1 -1
  165. package/src/flow/actions/open_ticket.ts +74 -3
  166. package/src/flow/actions/set_run_result.ts +83 -0
  167. package/src/flow/config.ts +4 -0
  168. package/src/flow/nodes/split_by_llm_categorize.ts +277 -0
  169. package/src/flow/nodes/split_by_ticket.ts +19 -0
  170. package/src/flow/nodes/wait_for_response.ts +28 -1
  171. package/src/flow/types.ts +26 -127
  172. package/src/form/ArrayEditor.ts +34 -82
  173. package/src/form/FieldRenderer.ts +465 -0
  174. package/src/form/FormField.ts +3 -3
  175. package/src/form/TextInput.ts +1 -1
  176. package/src/form/select/Select.ts +51 -20
  177. package/src/live/ContactChat.ts +39 -15
  178. package/src/markdown.ts +19 -11
  179. package/src/store/flow-definition.d.ts +5 -2
  180. package/static/api/labels.json +31 -0
  181. package/static/api/topics.json +24 -9
  182. package/static/api/users.json +35 -16
  183. package/static/css/temba-components.css +3 -3
  184. package/stress-test.js +18 -13
  185. package/test/ActionHelper.ts +2 -0
  186. package/test/NodeHelper.ts +184 -0
  187. package/test/actions/call_llm.test.ts +137 -0
  188. package/test/nodes/README.md +78 -0
  189. package/test/nodes/split_by_llm_categorize.test.ts +698 -0
  190. package/test/nodes/split_by_random.test.ts +177 -0
  191. package/test/nodes/wait_for_digits.test.ts +176 -0
  192. package/test/nodes/wait_for_response.test.ts +206 -0
  193. package/test/temba-add-input-labels.test.ts +87 -0
  194. package/test/temba-field-renderer.test.ts +482 -0
  195. package/test/temba-markdown.test.ts +1 -1
  196. package/test/temba-node-editor.test.ts +496 -0
  197. package/test/temba-select.test.ts +6 -6
  198. package/test/temba-webchat.test.ts +1 -1
  199. package/test-assets/select/llms.json +18 -0
  200. package/web-dev-mock.mjs +96 -6
  201. package/web-dev-server.config.mjs +29 -7
  202. package/test/temba-flow-editor.test.ts.backup +0 -563
  203. package/test/temba-utils-index.test.ts.backup +0 -1737
@@ -0,0 +1,103 @@
1
+ import { expect } from '@open-wc/testing';
2
+ import { call_llm } from '../../src/flow/actions/call_llm';
3
+ import { ActionTest } from '../ActionHelper';
4
+ /**
5
+ * Test suite for the call_llm action configuration.
6
+ */
7
+ describe('call_llm action config', () => {
8
+ const helper = new ActionTest(call_llm, 'call_llm');
9
+ describe('basic properties', () => {
10
+ helper.testBasicProperties();
11
+ it('has correct name', () => {
12
+ expect(call_llm.name).to.equal('Call AI');
13
+ });
14
+ it('has form configuration', () => {
15
+ expect(call_llm.form).to.exist;
16
+ expect(call_llm.form.llm).to.exist;
17
+ expect(call_llm.form.instructions).to.exist;
18
+ expect(call_llm.form.input).to.exist;
19
+ });
20
+ it('has layout configuration', () => {
21
+ expect(call_llm.layout).to.exist;
22
+ expect(call_llm.layout).to.deep.equal(['llm', 'input', 'instructions']);
23
+ });
24
+ it('has data transformation functions', () => {
25
+ expect(call_llm.toFormData).to.be.a('function');
26
+ expect(call_llm.fromFormData).to.be.a('function');
27
+ });
28
+ });
29
+ describe('data transformations', () => {
30
+ it('converts action to form data correctly', () => {
31
+ const action = {
32
+ uuid: 'test-llm-1',
33
+ type: 'call_llm',
34
+ input: '@input',
35
+ llm: { uuid: 'gpt-4', name: 'GPT 4.1' },
36
+ instructions: 'Translate to French',
37
+ result_name: 'translated_text'
38
+ };
39
+ const formData = call_llm.toFormData(action);
40
+ expect(formData.uuid).to.equal('test-llm-1');
41
+ expect(formData.llm).to.deep.equal([{ value: 'gpt-4', name: 'GPT 4.1' }]);
42
+ expect(formData.instructions).to.equal('Translate to French');
43
+ expect(formData.input).to.equal('@input');
44
+ });
45
+ it('converts form data to action correctly', () => {
46
+ const formData = {
47
+ uuid: 'test-llm-2',
48
+ llm: [{ value: 'gpt-5', name: 'GPT 5' }],
49
+ instructions: 'Summarize the following text',
50
+ input: '@input'
51
+ };
52
+ const action = call_llm.fromFormData(formData);
53
+ expect(action.uuid).to.equal('test-llm-2');
54
+ expect(action.type).to.equal('call_llm');
55
+ expect(action.llm).to.deep.equal({ uuid: 'gpt-5', name: 'GPT 5' });
56
+ expect(action.instructions).to.equal('Summarize the following text');
57
+ expect(action.input).to.equal('@input');
58
+ });
59
+ it('handles empty form data', () => {
60
+ const formData = {
61
+ uuid: 'test-llm-3',
62
+ llm: [],
63
+ instructions: '',
64
+ input: ''
65
+ };
66
+ const action = call_llm.fromFormData(formData);
67
+ expect(action.llm).to.deep.equal({ uuid: '', name: '' });
68
+ expect(action.instructions).to.equal('');
69
+ expect(action.input).to.equal('@input');
70
+ });
71
+ });
72
+ describe('action scenarios', () => {
73
+ helper.testAction({
74
+ uuid: 'test-action-1',
75
+ type: 'call_llm',
76
+ llm: { uuid: 'gpt-4', name: 'GPT 4.1' },
77
+ instructions: 'Translate to French',
78
+ input: '@input'
79
+ }, 'translation-task');
80
+ helper.testAction({
81
+ uuid: 'test-action-2',
82
+ type: 'call_llm',
83
+ llm: { uuid: 'gpt-5', name: 'GPT 5' },
84
+ instructions: 'Analyze the sentiment of the following message and classify it as positive, negative, or neutral. Provide a brief explanation for your classification.',
85
+ input: '@input'
86
+ }, 'sentiment-analysis');
87
+ helper.testAction({
88
+ uuid: 'test-action-3',
89
+ type: 'call_llm',
90
+ llm: { uuid: 'gpt-4', name: 'GPT 4.1' },
91
+ instructions: 'Summarize the key points from the conversation above in bullet format.',
92
+ input: '@input'
93
+ }, 'summarization');
94
+ helper.testAction({
95
+ uuid: 'test-action-4',
96
+ type: 'call_llm',
97
+ llm: { uuid: 'gpt-5', name: 'GPT 5' },
98
+ instructions: 'Extract any contact information (phone numbers, email addresses) from the text and format them as a JSON object.',
99
+ input: '@input'
100
+ }, 'information-extraction');
101
+ });
102
+ });
103
+ //# sourceMappingURL=call_llm.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"call_llm.test.js","sourceRoot":"","sources":["../../../test/actions/call_llm.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { expect } from '@open-wc/testing';\nimport { call_llm } from '../../src/flow/actions/call_llm';\nimport { CallLLM } from '../../src/store/flow-definition';\nimport { ActionTest } from '../ActionHelper';\n\n/**\n * Test suite for the call_llm action configuration.\n */\ndescribe('call_llm action config', () => {\n const helper = new ActionTest(call_llm, 'call_llm');\n\n describe('basic properties', () => {\n helper.testBasicProperties();\n\n it('has correct name', () => {\n expect(call_llm.name).to.equal('Call AI');\n });\n\n it('has form configuration', () => {\n expect(call_llm.form).to.exist;\n expect(call_llm.form.llm).to.exist;\n expect(call_llm.form.instructions).to.exist;\n expect(call_llm.form.input).to.exist;\n });\n\n it('has layout configuration', () => {\n expect(call_llm.layout).to.exist;\n expect(call_llm.layout).to.deep.equal(['llm', 'input', 'instructions']);\n });\n\n it('has data transformation functions', () => {\n expect(call_llm.toFormData).to.be.a('function');\n expect(call_llm.fromFormData).to.be.a('function');\n });\n });\n\n describe('data transformations', () => {\n it('converts action to form data correctly', () => {\n const action: CallLLM = {\n uuid: 'test-llm-1',\n type: 'call_llm',\n input: '@input',\n llm: { uuid: 'gpt-4', name: 'GPT 4.1' },\n instructions: 'Translate to French',\n result_name: 'translated_text'\n };\n\n const formData = call_llm.toFormData(action);\n\n expect(formData.uuid).to.equal('test-llm-1');\n expect(formData.llm).to.deep.equal([{ value: 'gpt-4', name: 'GPT 4.1' }]);\n expect(formData.instructions).to.equal('Translate to French');\n expect(formData.input).to.equal('@input');\n });\n\n it('converts form data to action correctly', () => {\n const formData = {\n uuid: 'test-llm-2',\n llm: [{ value: 'gpt-5', name: 'GPT 5' }],\n instructions: 'Summarize the following text',\n input: '@input'\n };\n\n const action = call_llm.fromFormData(formData) as CallLLM;\n\n expect(action.uuid).to.equal('test-llm-2');\n expect(action.type).to.equal('call_llm');\n expect(action.llm).to.deep.equal({ uuid: 'gpt-5', name: 'GPT 5' });\n expect(action.instructions).to.equal('Summarize the following text');\n expect(action.input).to.equal('@input');\n });\n\n it('handles empty form data', () => {\n const formData = {\n uuid: 'test-llm-3',\n llm: [],\n instructions: '',\n input: ''\n };\n\n const action = call_llm.fromFormData(formData) as CallLLM;\n\n expect(action.llm).to.deep.equal({ uuid: '', name: '' });\n expect(action.instructions).to.equal('');\n expect(action.input).to.equal('@input');\n });\n });\n\n describe('action scenarios', () => {\n helper.testAction(\n {\n uuid: 'test-action-1',\n type: 'call_llm',\n llm: { uuid: 'gpt-4', name: 'GPT 4.1' },\n instructions: 'Translate to French',\n input: '@input'\n } as CallLLM,\n 'translation-task'\n );\n\n helper.testAction(\n {\n uuid: 'test-action-2',\n type: 'call_llm',\n llm: { uuid: 'gpt-5', name: 'GPT 5' },\n instructions:\n 'Analyze the sentiment of the following message and classify it as positive, negative, or neutral. Provide a brief explanation for your classification.',\n input: '@input'\n } as CallLLM,\n 'sentiment-analysis'\n );\n\n helper.testAction(\n {\n uuid: 'test-action-3',\n type: 'call_llm',\n llm: { uuid: 'gpt-4', name: 'GPT 4.1' },\n instructions:\n 'Summarize the key points from the conversation above in bullet format.',\n input: '@input'\n } as CallLLM,\n 'summarization'\n );\n\n helper.testAction(\n {\n uuid: 'test-action-4',\n type: 'call_llm',\n llm: { uuid: 'gpt-5', name: 'GPT 5' },\n instructions:\n 'Extract any contact information (phone numbers, email addresses) from the text and format them as a JSON object.',\n input: '@input'\n } as CallLLM,\n 'information-extraction'\n );\n });\n});\n"]}
@@ -0,0 +1,532 @@
1
+ import { expect } from '@open-wc/testing';
2
+ import { split_by_llm_categorize } from '../../src/flow/nodes/split_by_llm_categorize';
3
+ import { NodeTest } from '../NodeHelper';
4
+ // Helper function to create routers with proper cases and exits
5
+ function createSplitRouter(categoryNames) {
6
+ const categories = [];
7
+ const exits = [];
8
+ const cases = [];
9
+ // Add user categories
10
+ categoryNames.forEach((categoryName) => {
11
+ const categoryUuid = `category-${categoryName
12
+ .toLowerCase()
13
+ .replace(/\s+/g, '-')}`;
14
+ const exitUuid = `exit-${categoryName.toLowerCase().replace(/\s+/g, '-')}`;
15
+ const caseUuid = `case-${categoryName.toLowerCase().replace(/\s+/g, '-')}`;
16
+ categories.push({
17
+ uuid: categoryUuid,
18
+ name: categoryName,
19
+ exit_uuid: exitUuid
20
+ });
21
+ exits.push({
22
+ uuid: exitUuid,
23
+ destination_uuid: null
24
+ });
25
+ cases.push({
26
+ uuid: caseUuid,
27
+ type: 'has_only_text',
28
+ arguments: [categoryName],
29
+ category_uuid: categoryUuid
30
+ });
31
+ });
32
+ // Add "Other" category (default)
33
+ const otherCategoryUuid = 'category-other';
34
+ const otherExitUuid = 'exit-other';
35
+ categories.push({
36
+ uuid: otherCategoryUuid,
37
+ name: 'Other',
38
+ exit_uuid: otherExitUuid
39
+ });
40
+ exits.push({
41
+ uuid: otherExitUuid,
42
+ destination_uuid: null
43
+ });
44
+ // Add "Failure" category
45
+ const failureCategoryUuid = 'category-failure';
46
+ const failureExitUuid = 'exit-failure';
47
+ const failureCaseUuid = 'case-failure';
48
+ categories.push({
49
+ uuid: failureCategoryUuid,
50
+ name: 'Failure',
51
+ exit_uuid: failureExitUuid
52
+ });
53
+ exits.push({
54
+ uuid: failureExitUuid,
55
+ destination_uuid: null
56
+ });
57
+ // Add failure case for <ERROR>
58
+ cases.push({
59
+ uuid: failureCaseUuid,
60
+ type: 'has_only_text',
61
+ arguments: ['<ERROR>'],
62
+ category_uuid: failureCategoryUuid
63
+ });
64
+ return {
65
+ router: {
66
+ type: 'switch',
67
+ categories: categories,
68
+ default_category_uuid: otherCategoryUuid,
69
+ operand: '@locals._llm_output',
70
+ cases: cases
71
+ },
72
+ exits: exits
73
+ };
74
+ }
75
+ /**
76
+ * Test suite for the split_by_llm_categorize node configuration.
77
+ */
78
+ describe('split_by_llm_categorize node config', () => {
79
+ const helper = new NodeTest(split_by_llm_categorize, 'split_by_llm_categorize');
80
+ describe('basic properties', () => {
81
+ helper.testBasicProperties();
82
+ it('has correct name', () => {
83
+ expect(split_by_llm_categorize.name).to.equal('Split by AI');
84
+ });
85
+ it('has correct type', () => {
86
+ expect(split_by_llm_categorize.type).to.equal('split_by_llm_categorize');
87
+ });
88
+ });
89
+ describe('node scenarios', () => {
90
+ const basicRouter = createSplitRouter(['Greeting', 'Question']);
91
+ helper.testNode({
92
+ uuid: 'test-node-1',
93
+ actions: [
94
+ {
95
+ uuid: 'call-llm-uuid',
96
+ type: 'call_llm',
97
+ llm: { uuid: 'llm-123', name: 'Claude' },
98
+ input: '@input',
99
+ instructions: '@(prompt("categorize", slice(node.categories, 0, -2)))',
100
+ output_local: '_llm_output'
101
+ }
102
+ ],
103
+ router: basicRouter.router,
104
+ exits: basicRouter.exits
105
+ }, { type: 'split_by_llm_categorize' }, 'basic-categorization');
106
+ const premiumRouter = createSplitRouter(['Premium', 'Regular', 'VIP']);
107
+ helper.testNode({
108
+ uuid: 'test-node-2',
109
+ actions: [
110
+ {
111
+ uuid: 'call-llm-uuid-2',
112
+ type: 'call_llm',
113
+ llm: { uuid: 'llm-456', name: 'GPT-4' },
114
+ input: '@contact.name',
115
+ instructions: '@(prompt("categorize", slice(node.categories, 0, -2)))',
116
+ output_local: '_llm_output'
117
+ }
118
+ ],
119
+ router: premiumRouter.router,
120
+ exits: premiumRouter.exits
121
+ }, { type: 'split_by_llm_categorize' }, 'custom-input-and-result-name');
122
+ const priorityRouter = createSplitRouter([
123
+ 'High',
124
+ 'Medium',
125
+ 'Low',
126
+ 'Critical',
127
+ 'Urgent'
128
+ ]);
129
+ helper.testNode({
130
+ uuid: 'test-node-3',
131
+ actions: [
132
+ {
133
+ uuid: 'call-llm-uuid-3',
134
+ type: 'call_llm',
135
+ llm: { uuid: 'llm-789', name: 'Gemini' },
136
+ input: '@fields.priority',
137
+ instructions: '@(prompt("categorize", slice(node.categories, 0, -2)))',
138
+ output_local: '_llm_output'
139
+ }
140
+ ],
141
+ router: priorityRouter.router,
142
+ exits: priorityRouter.exits
143
+ }, { type: 'split_by_llm_categorize' }, 'many-categories');
144
+ const minimalRouter = createSplitRouter(['Yes']);
145
+ helper.testNode({
146
+ uuid: 'test-node-4',
147
+ actions: [
148
+ {
149
+ uuid: 'call-llm-uuid-4',
150
+ type: 'call_llm',
151
+ llm: { uuid: 'llm-minimal', name: 'Basic LLM' },
152
+ input: '@input',
153
+ instructions: '@(prompt("categorize", slice(node.categories, 0, -2)))',
154
+ output_local: '_llm_output'
155
+ }
156
+ ],
157
+ router: minimalRouter.router,
158
+ exits: minimalRouter.exits
159
+ }, { type: 'split_by_llm_categorize' }, 'minimal-categories');
160
+ const feedbackRouter = createSplitRouter([
161
+ 'Bug Report',
162
+ 'Feature Request',
163
+ 'General Feedback',
164
+ 'Support Request'
165
+ ]);
166
+ helper.testNode({
167
+ uuid: 'test-node-5',
168
+ actions: [
169
+ {
170
+ uuid: 'call-llm-uuid-5',
171
+ type: 'call_llm',
172
+ llm: { uuid: 'llm-special', name: 'Special Characters LLM' },
173
+ input: '@contact.fields.feedback',
174
+ instructions: '@(prompt("categorize", slice(node.categories, 0, -2)))',
175
+ output_local: '_llm_output'
176
+ }
177
+ ],
178
+ router: feedbackRouter.router,
179
+ exits: feedbackRouter.exits
180
+ }, { type: 'split_by_llm_categorize' }, 'feedback-categorization');
181
+ });
182
+ describe('round-trip conversion validation', () => {
183
+ it('converts to form data correctly', () => {
184
+ const testRouter = createSplitRouter(['Greeting', 'Question']);
185
+ const node = {
186
+ uuid: 'test-node',
187
+ actions: [
188
+ {
189
+ uuid: 'call-llm-uuid',
190
+ type: 'call_llm',
191
+ llm: { uuid: 'llm-123', name: 'Test LLM' },
192
+ input: '@input',
193
+ instructions: '@(prompt("categorize", slice(node.categories, 0, -2)))',
194
+ output_local: '_llm_output'
195
+ }
196
+ ],
197
+ router: testRouter.router,
198
+ exits: testRouter.exits
199
+ };
200
+ const formData = split_by_llm_categorize.toFormData(node);
201
+ expect(formData.uuid).to.equal('test-node');
202
+ expect(formData.llm).to.deep.equal([
203
+ { value: 'llm-123', name: 'Test LLM' }
204
+ ]);
205
+ expect(formData.input).to.equal('@input');
206
+ expect(formData.categories).to.deep.equal([
207
+ { name: 'Greeting' },
208
+ { name: 'Question' }
209
+ ]);
210
+ });
211
+ it('converts from form data correctly', () => {
212
+ const formData = {
213
+ uuid: 'test-node',
214
+ llm: [{ value: 'llm-456', name: 'GPT-4' }],
215
+ input: '@contact.name',
216
+ categories: [{ name: 'Premium' }, { name: 'Regular' }]
217
+ };
218
+ const originalNode = {
219
+ uuid: 'test-node',
220
+ actions: [],
221
+ exits: []
222
+ };
223
+ const result = split_by_llm_categorize.fromFormData(formData, originalNode);
224
+ expect(result.uuid).to.equal('test-node');
225
+ expect(result.actions).to.have.length(1);
226
+ expect(result.actions[0].type).to.equal('call_llm');
227
+ expect(result.actions[0].llm.uuid).to.equal('llm-456');
228
+ expect(result.actions[0].llm.name).to.equal('GPT-4');
229
+ expect(result.actions[0].input).to.equal('@contact.name');
230
+ // Should have user categories plus Other and Failure
231
+ expect(result.router.categories).to.have.length(4);
232
+ const categoryNames = result.router.categories.map((cat) => cat.name);
233
+ expect(categoryNames).to.include.members([
234
+ 'Premium',
235
+ 'Regular',
236
+ 'Other',
237
+ 'Failure'
238
+ ]);
239
+ // Should have corresponding exits
240
+ expect(result.exits).to.have.length(4);
241
+ });
242
+ });
243
+ describe('edge cases and validation', () => {
244
+ it('handles categories with empty names correctly', () => {
245
+ const formData = {
246
+ uuid: 'test-node-uuid',
247
+ llm: [{ value: 'llm-uuid-123', name: 'Claude' }],
248
+ input: '@input',
249
+ categories: [
250
+ { name: 'Valid Category' },
251
+ { name: '' }, // empty name
252
+ { name: ' ' }, // only whitespace
253
+ { name: 'Another Valid' }
254
+ ],
255
+ result_name: 'Intent'
256
+ };
257
+ const originalNode = {
258
+ uuid: 'test-node-uuid',
259
+ actions: [],
260
+ exits: []
261
+ };
262
+ const result = split_by_llm_categorize.fromFormData(formData, originalNode);
263
+ // Should only include non-empty categories
264
+ const userCategories = result.router.categories.filter((cat) => cat.name !== 'Other' && cat.name !== 'Failure');
265
+ expect(userCategories).to.have.length(2);
266
+ expect(userCategories.map((cat) => cat.name)).to.deep.equal([
267
+ 'Valid Category',
268
+ 'Another Valid'
269
+ ]);
270
+ });
271
+ it('handles categories with special characters', () => {
272
+ const formData = {
273
+ uuid: 'test-node-uuid',
274
+ llm: [{ value: 'llm-uuid-123', name: 'Claude' }],
275
+ input: '@input',
276
+ categories: [
277
+ { name: 'Category-1' },
278
+ { name: 'Category_2' },
279
+ { name: 'Category@3' },
280
+ { name: 'Category with spaces' }
281
+ ],
282
+ result_name: 'Intent'
283
+ };
284
+ const originalNode = {
285
+ uuid: 'test-node-uuid',
286
+ actions: [],
287
+ exits: []
288
+ };
289
+ const result = split_by_llm_categorize.fromFormData(formData, originalNode);
290
+ // Should preserve all special characters in category names
291
+ const userCategories = result.router.categories.filter((cat) => cat.name !== 'Other' && cat.name !== 'Failure');
292
+ expect(userCategories).to.have.length(4);
293
+ expect(userCategories.map((cat) => cat.name)).to.include.members([
294
+ 'Category-1',
295
+ 'Category_2',
296
+ 'Category@3',
297
+ 'Category with spaces'
298
+ ]);
299
+ // Verify cases also have correct names
300
+ const caseNames = result
301
+ .router.cases.filter((c) => c.arguments[0] !== '<ERROR>')
302
+ .map((c) => c.arguments[0]);
303
+ expect(caseNames).to.include.members([
304
+ 'Category-1',
305
+ 'Category_2',
306
+ 'Category@3',
307
+ 'Category with spaces'
308
+ ]);
309
+ });
310
+ it('maintains UUID consistency between categories, cases, and exits', () => {
311
+ const formData = {
312
+ uuid: 'test-node-uuid',
313
+ llm: [{ value: 'llm-uuid-123', name: 'Claude' }],
314
+ input: '@input',
315
+ categories: [{ name: 'Test Category' }],
316
+ result_name: 'Intent'
317
+ };
318
+ const originalNode = {
319
+ uuid: 'test-node-uuid',
320
+ actions: [],
321
+ exits: []
322
+ };
323
+ const result = split_by_llm_categorize.fromFormData(formData, originalNode);
324
+ // Find the test category
325
+ const testCategory = result.router.categories.find((cat) => cat.name === 'Test Category');
326
+ const testCase = result.router.cases.find((c) => c.arguments[0] === 'Test Category');
327
+ const testExit = result.exits.find((exit) => exit.uuid === testCategory.exit_uuid);
328
+ // Verify UUID consistency
329
+ expect(testCase.category_uuid).to.equal(testCategory.uuid);
330
+ expect(testExit.uuid).to.equal(testCategory.exit_uuid);
331
+ });
332
+ it('generates unique UUIDs for each run', () => {
333
+ const formData = {
334
+ uuid: 'test-node-uuid',
335
+ llm: [{ value: 'llm-uuid-123', name: 'Claude' }],
336
+ input: '@input',
337
+ categories: [{ name: 'Test' }],
338
+ result_name: 'Intent'
339
+ };
340
+ const originalNode = {
341
+ uuid: 'test-node-uuid',
342
+ actions: [],
343
+ exits: []
344
+ };
345
+ const result1 = split_by_llm_categorize.fromFormData(formData, originalNode);
346
+ const result2 = split_by_llm_categorize.fromFormData(formData, originalNode);
347
+ // UUIDs should be different for each generation
348
+ expect(result1.actions[0].uuid).to.not.equal(result2.actions[0].uuid);
349
+ expect(result1.router.categories[0].uuid).to.not.equal(result2.router.categories[0].uuid);
350
+ expect(result1.exits[0].uuid).to.not.equal(result2.exits[0].uuid);
351
+ });
352
+ it('roundtrip conversion (fromFormData -> toFormData) works correctly', () => {
353
+ const originalFormData = {
354
+ uuid: 'test-node-uuid',
355
+ llm: [{ value: 'llm-uuid-123', name: 'Claude' }],
356
+ input: '@custom.input',
357
+ categories: [{ name: 'Category1' }, { name: 'Category2' }],
358
+ result_name: 'CustomResult'
359
+ };
360
+ const originalNode = {
361
+ uuid: 'test-node-uuid',
362
+ actions: [],
363
+ exits: []
364
+ };
365
+ // Convert form data to node
366
+ const node = split_by_llm_categorize.fromFormData(originalFormData, originalNode);
367
+ // Convert back to form data
368
+ const recoveredFormData = split_by_llm_categorize.toFormData(node);
369
+ // Should match original data
370
+ expect(recoveredFormData.uuid).to.equal(originalFormData.uuid);
371
+ expect(recoveredFormData.llm).to.deep.equal(originalFormData.llm);
372
+ expect(recoveredFormData.input).to.equal(originalFormData.input);
373
+ expect(recoveredFormData.categories).to.deep.equal(originalFormData.categories);
374
+ });
375
+ it('handles max 10 categories requirement', () => {
376
+ // Create 12 categories to test the limit
377
+ const categories = Array.from({ length: 12 }, (_, i) => ({
378
+ name: `Category${i + 1}`
379
+ }));
380
+ const formData = {
381
+ uuid: 'test-node-uuid',
382
+ llm: [{ value: 'llm-uuid-123', name: 'Claude' }],
383
+ input: '@input',
384
+ categories: categories,
385
+ result_name: 'Intent'
386
+ };
387
+ const originalNode = {
388
+ uuid: 'test-node-uuid',
389
+ actions: [],
390
+ exits: []
391
+ };
392
+ const result = split_by_llm_categorize.fromFormData(formData, originalNode);
393
+ // Should process all categories provided (fromFormData doesn't enforce the limit, validation should)
394
+ const userCategories = result.router.categories.filter((cat) => cat.name !== 'Other' && cat.name !== 'Failure');
395
+ expect(userCategories).to.have.length(12);
396
+ // Note: The actual 10-category limit should be enforced by the UI validation
397
+ // which uses the maxItems: 10 property in the form configuration
398
+ });
399
+ it('preserves original node UUID', () => {
400
+ const formData = {
401
+ uuid: 'should-be-ignored',
402
+ llm: [{ value: 'llm-uuid-123', name: 'Claude' }],
403
+ input: '@input',
404
+ categories: [{ name: 'Test' }],
405
+ result_name: 'Intent'
406
+ };
407
+ const originalNode = {
408
+ uuid: 'original-node-uuid',
409
+ actions: [],
410
+ exits: []
411
+ };
412
+ const result = split_by_llm_categorize.fromFormData(formData, originalNode);
413
+ // Should use original node UUID, not the one from form data
414
+ expect(result.uuid).to.equal('original-node-uuid');
415
+ });
416
+ });
417
+ describe('validation', () => {
418
+ it('should validate duplicate category names', () => {
419
+ const formData = {
420
+ uuid: 'test-node-uuid',
421
+ llm: [{ value: 'llm-uuid-123', name: 'Claude' }],
422
+ input: '@input',
423
+ categories: [
424
+ { name: 'Category1' },
425
+ { name: 'Category2' },
426
+ { name: 'Category1' }, // duplicate
427
+ { name: 'category2' }, // case insensitive duplicate
428
+ { name: 'Category3' }
429
+ ]
430
+ };
431
+ const validationResult = split_by_llm_categorize.validate(formData);
432
+ expect(validationResult.valid).to.be.false;
433
+ expect(validationResult.errors.categories).to.include('Duplicate category names found');
434
+ expect(validationResult.errors.categories).to.include('Category1');
435
+ expect(validationResult.errors.categories).to.include('Category2');
436
+ expect(validationResult.errors.categories).to.include('category2');
437
+ });
438
+ it('should pass validation with unique category names', () => {
439
+ const formData = {
440
+ uuid: 'test-node-uuid',
441
+ llm: [{ value: 'llm-uuid-123', name: 'Claude' }],
442
+ input: '@input',
443
+ categories: [
444
+ { name: 'Category1' },
445
+ { name: 'Category2' },
446
+ { name: 'Category3' }
447
+ ]
448
+ };
449
+ const validationResult = split_by_llm_categorize.validate(formData);
450
+ expect(validationResult.valid).to.be.true;
451
+ expect(Object.keys(validationResult.errors)).to.have.length(0);
452
+ });
453
+ it('should ignore empty categories in validation', () => {
454
+ const formData = {
455
+ uuid: 'test-node-uuid',
456
+ llm: [{ value: 'llm-uuid-123', name: 'Claude' }],
457
+ input: '@input',
458
+ categories: [
459
+ { name: 'Category1' },
460
+ { name: '' }, // empty
461
+ { name: ' ' }, // whitespace only
462
+ { name: 'Category2' }
463
+ ]
464
+ };
465
+ const validationResult = split_by_llm_categorize.validate(formData);
466
+ expect(validationResult.valid).to.be.true;
467
+ expect(Object.keys(validationResult.errors)).to.have.length(0);
468
+ });
469
+ });
470
+ describe('JSON output verification', () => {
471
+ it('generates JSON matching the exact format from the issue', () => {
472
+ const formData = {
473
+ uuid: '145eb3d3-b841-4e66-abac-297ae525c7ad',
474
+ llm: [
475
+ { value: '1c06c884-39dd-4ce4-ad9f-9a01cbe6c000', name: 'Claude' }
476
+ ],
477
+ input: '@input',
478
+ categories: [{ name: 'Flights' }, { name: 'Hotels' }],
479
+ result_name: 'Intent'
480
+ };
481
+ const originalNode = {
482
+ uuid: '145eb3d3-b841-4e66-abac-297ae525c7ad',
483
+ actions: [],
484
+ exits: []
485
+ };
486
+ const result = split_by_llm_categorize.fromFormData(formData, originalNode);
487
+ // Verify the call_llm action
488
+ const callLlmAction = result.actions[0];
489
+ expect(callLlmAction.type).to.equal('call_llm');
490
+ expect(callLlmAction.llm.uuid).to.equal('1c06c884-39dd-4ce4-ad9f-9a01cbe6c000');
491
+ expect(callLlmAction.llm.name).to.equal('Claude');
492
+ expect(callLlmAction.instructions).to.equal('@(prompt("categorize", slice(node.categories, 0, -2)))');
493
+ expect(callLlmAction.input).to.equal('@input');
494
+ expect(callLlmAction.output_local).to.equal('_llm_output');
495
+ // Verify the router structure
496
+ const router = result.router;
497
+ expect(router.type).to.equal('switch');
498
+ expect(router.operand).to.equal('@locals._llm_output');
499
+ // Verify categories structure
500
+ expect(router.categories).to.have.length(4);
501
+ const categoryNames = router.categories.map((cat) => cat.name);
502
+ expect(categoryNames).to.include.members([
503
+ 'Flights',
504
+ 'Hotels',
505
+ 'Other',
506
+ 'Failure'
507
+ ]);
508
+ // Verify cases structure
509
+ expect(router.cases).to.have.length(3);
510
+ const caseArguments = router.cases.map((c) => c.arguments[0]);
511
+ expect(caseArguments).to.include.members([
512
+ 'Flights',
513
+ 'Hotels',
514
+ '<ERROR>'
515
+ ]);
516
+ // Verify all cases use has_only_text
517
+ router.cases.forEach((caseItem) => {
518
+ expect(caseItem.type).to.equal('has_only_text');
519
+ });
520
+ // Verify exits match categories
521
+ expect(result.exits).to.have.length(4);
522
+ router.categories.forEach((category) => {
523
+ const matchingExit = result.exits.find((exit) => exit.uuid === category.exit_uuid);
524
+ expect(matchingExit).to.exist;
525
+ });
526
+ // Verify default category is "Other"
527
+ const otherCategory = router.categories.find((cat) => cat.name === 'Other');
528
+ expect(router.default_category_uuid).to.equal(otherCategory.uuid);
529
+ });
530
+ });
531
+ });
532
+ //# sourceMappingURL=split_by_llm_categorize.test.js.map