@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
@@ -0,0 +1,277 @@
1
+ import { COLORS, NodeConfig } from '../types';
2
+ import { Node } from '../../store/flow-definition';
3
+ import { generateUUID } from '../../utils';
4
+ import { html } from 'lit';
5
+
6
+ export const split_by_llm_categorize: NodeConfig = {
7
+ type: 'split_by_llm_categorize',
8
+ name: 'Split by AI',
9
+ color: COLORS.call,
10
+ form: {
11
+ llm: {
12
+ type: 'select',
13
+ label: 'LLM',
14
+ helpText: 'Select the LLM to use for categorization',
15
+ required: true,
16
+ endpoint: '/test-assets/select/llms.json',
17
+ valueKey: 'uuid',
18
+ nameKey: 'name',
19
+ placeholder: 'Select an LLM...'
20
+ },
21
+ input: {
22
+ type: 'text',
23
+ label: 'Input',
24
+ helpText: 'The input to categorize (usually @input)',
25
+ required: true,
26
+ evaluated: true,
27
+ placeholder: '@input'
28
+ },
29
+ categories: {
30
+ type: 'array',
31
+ label: 'Categories',
32
+ helpText: 'Define the categories for classification',
33
+ required: true,
34
+ itemLabel: 'Category',
35
+ minItems: 1,
36
+ maxItems: 10,
37
+ isEmptyItem: (item: any) => {
38
+ return !item.name || item.name.trim() === '';
39
+ },
40
+ itemConfig: {
41
+ name: {
42
+ type: 'text',
43
+ placeholder: 'Category name',
44
+ required: true
45
+ }
46
+ }
47
+ }
48
+ },
49
+ layout: ['llm', 'input', 'categories'],
50
+ validate: (formData: any) => {
51
+ const errors: { [key: string]: string } = {};
52
+
53
+ // Check for duplicate category names
54
+ if (formData.categories && Array.isArray(formData.categories)) {
55
+ const categories = formData.categories.filter(
56
+ (item: any) => item?.name && item.name.trim() !== ''
57
+ );
58
+
59
+ // Find all categories that have duplicates (case-insensitive)
60
+ const duplicateCategories = [];
61
+ const lowerCaseMap = new Map();
62
+
63
+ // First pass: map lowercase names to all original cases
64
+ categories.forEach((category) => {
65
+ const lowerName = category.name.trim().toLowerCase();
66
+ if (!lowerCaseMap.has(lowerName)) {
67
+ lowerCaseMap.set(lowerName, []);
68
+ }
69
+ lowerCaseMap.get(lowerName).push(category.name.trim());
70
+ });
71
+
72
+ // Second pass: collect all names that appear more than once
73
+ lowerCaseMap.forEach((originalNames) => {
74
+ if (originalNames.length > 1) {
75
+ duplicateCategories.push(...originalNames);
76
+ }
77
+ });
78
+
79
+ if (duplicateCategories.length > 0) {
80
+ const uniqueDuplicates = [...new Set(duplicateCategories)];
81
+ errors.categories = `Duplicate category names found: ${uniqueDuplicates.join(
82
+ ', '
83
+ )}`;
84
+ }
85
+ }
86
+
87
+ return {
88
+ valid: Object.keys(errors).length === 0,
89
+ errors
90
+ };
91
+ },
92
+ render: (node: Node) => {
93
+ const callLlmAction = node.actions?.find(
94
+ (action) => action.type === 'call_llm'
95
+ ) as any;
96
+ return html`
97
+ <div class="body">Categorize with ${callLlmAction.llm.name}</div>
98
+ `;
99
+ },
100
+ toFormData: (node: Node) => {
101
+ // Extract data from the existing node structure
102
+ const callLlmAction = node.actions?.find(
103
+ (action) => action.type === 'call_llm'
104
+ ) as any;
105
+ const categories =
106
+ node.router?.categories
107
+ ?.filter((cat) => cat.name !== 'Other' && cat.name !== 'Failure')
108
+ .map((cat) => ({ name: cat.name })) || [];
109
+
110
+ return {
111
+ uuid: node.uuid,
112
+ llm: callLlmAction?.llm
113
+ ? [{ value: callLlmAction.llm.uuid, name: callLlmAction.llm.name }]
114
+ : [],
115
+ input: callLlmAction?.input || '@input',
116
+ categories: categories
117
+ };
118
+ },
119
+ fromFormData: (formData: any, originalNode: Node): Node => {
120
+ // Get LLM selection
121
+ const llmSelection =
122
+ Array.isArray(formData.llm) && formData.llm.length > 0
123
+ ? formData.llm[0]
124
+ : null;
125
+
126
+ // Get user categories
127
+ const userCategories = (formData.categories || [])
128
+ .filter((item: any) => item?.name?.trim())
129
+ .map((item: any) => item.name.trim());
130
+
131
+ // Find existing call_llm action to preserve its UUID
132
+ const existingCallLlmAction = originalNode.actions?.find(
133
+ (action) => action.type === 'call_llm'
134
+ );
135
+ const callLlmUuid = existingCallLlmAction?.uuid || generateUUID();
136
+
137
+ // Create call_llm action (using any type to match the example format)
138
+ const callLlmAction: any = {
139
+ type: 'call_llm',
140
+ uuid: callLlmUuid,
141
+ llm: llmSelection
142
+ ? { uuid: llmSelection.value, name: llmSelection.name }
143
+ : { uuid: '', name: '' },
144
+ instructions: `@(prompt("categorize", slice(node.categories, 0, -2)))`,
145
+ input: formData.input || '@input',
146
+ output_local: '_llm_output'
147
+ };
148
+
149
+ // Create categories and exits
150
+ const categories = [];
151
+ const exits = [];
152
+ const cases = [];
153
+
154
+ // Get existing categories from original node for UUID preservation
155
+ const existingCategories = originalNode.router?.categories || [];
156
+ const existingExits = originalNode.exits || [];
157
+ const existingCases = originalNode.router?.cases || [];
158
+
159
+ // Add user categories
160
+ userCategories.forEach((categoryName: string) => {
161
+ // Check if this category already exists
162
+ const existingCategory = existingCategories.find(
163
+ (cat) => cat.name === categoryName
164
+ );
165
+ const existingExit = existingCategory
166
+ ? existingExits.find((exit) => exit.uuid === existingCategory.exit_uuid)
167
+ : null;
168
+ const existingCase = existingCategory
169
+ ? existingCases.find(
170
+ (case_) => case_.category_uuid === existingCategory.uuid
171
+ )
172
+ : null;
173
+
174
+ // Use existing UUIDs if category name hasn't changed, otherwise generate new ones
175
+ const categoryUuid = existingCategory?.uuid || generateUUID();
176
+ const exitUuid = existingExit?.uuid || generateUUID();
177
+ const caseUuid = existingCase?.uuid || generateUUID();
178
+
179
+ categories.push({
180
+ uuid: categoryUuid,
181
+ name: categoryName,
182
+ exit_uuid: exitUuid
183
+ });
184
+
185
+ exits.push({
186
+ uuid: exitUuid,
187
+ destination_uuid: existingExit?.destination_uuid || null
188
+ });
189
+
190
+ cases.push({
191
+ uuid: caseUuid,
192
+ type: 'has_only_text',
193
+ arguments: [categoryName],
194
+ category_uuid: categoryUuid
195
+ });
196
+ });
197
+
198
+ // Add "Other" category (default)
199
+ const existingOtherCategory = existingCategories.find(
200
+ (cat) => cat.name === 'Other'
201
+ );
202
+ const existingOtherExit = existingOtherCategory
203
+ ? existingExits.find(
204
+ (exit) => exit.uuid === existingOtherCategory.exit_uuid
205
+ )
206
+ : null;
207
+
208
+ const otherCategoryUuid = existingOtherCategory?.uuid || generateUUID();
209
+ const otherExitUuid = existingOtherExit?.uuid || generateUUID();
210
+
211
+ categories.push({
212
+ uuid: otherCategoryUuid,
213
+ name: 'Other',
214
+ exit_uuid: otherExitUuid
215
+ });
216
+ exits.push({
217
+ uuid: otherExitUuid,
218
+ destination_uuid: existingOtherExit?.destination_uuid || null
219
+ });
220
+
221
+ // Add "Failure" category
222
+ const existingFailureCategory = existingCategories.find(
223
+ (cat) => cat.name === 'Failure'
224
+ );
225
+ const existingFailureExit = existingFailureCategory
226
+ ? existingExits.find(
227
+ (exit) => exit.uuid === existingFailureCategory.exit_uuid
228
+ )
229
+ : null;
230
+ const existingFailureCase = existingFailureCategory
231
+ ? existingCases.find(
232
+ (case_) =>
233
+ case_.category_uuid === existingFailureCategory.uuid &&
234
+ case_.arguments?.[0] === '<ERROR>'
235
+ )
236
+ : null;
237
+
238
+ const failureCategoryUuid = existingFailureCategory?.uuid || generateUUID();
239
+ const failureExitUuid = existingFailureExit?.uuid || generateUUID();
240
+ const failureCaseUuid = existingFailureCase?.uuid || generateUUID();
241
+
242
+ categories.push({
243
+ uuid: failureCategoryUuid,
244
+ name: 'Failure',
245
+ exit_uuid: failureExitUuid
246
+ });
247
+ exits.push({
248
+ uuid: failureExitUuid,
249
+ destination_uuid: existingFailureExit?.destination_uuid || null
250
+ });
251
+
252
+ // Add failure case for <ERROR>
253
+ cases.push({
254
+ uuid: failureCaseUuid,
255
+ type: 'has_only_text',
256
+ arguments: ['<ERROR>'],
257
+ category_uuid: failureCategoryUuid
258
+ });
259
+
260
+ // Create the router
261
+ const router = {
262
+ type: 'switch' as const,
263
+ categories: categories,
264
+ default_category_uuid: otherCategoryUuid,
265
+ operand: '@locals._llm_output',
266
+ cases: cases
267
+ };
268
+
269
+ // Return the complete node
270
+ return {
271
+ uuid: originalNode.uuid,
272
+ actions: [callLlmAction],
273
+ router: router,
274
+ exits: exits
275
+ };
276
+ }
277
+ };
@@ -0,0 +1,19 @@
1
+ import { open_ticket } from '../actions/open_ticket';
2
+ import { NodeConfig } from '../types';
3
+
4
+ export const split_by_ticket: NodeConfig = {
5
+ type: 'split_by_ticket',
6
+ action: open_ticket,
7
+ router: {
8
+ type: 'switch',
9
+ defaultCategory: 'Failure',
10
+ operand: '@locals._new_ticket',
11
+ rules: [
12
+ {
13
+ type: 'has_text',
14
+ arguments: [],
15
+ categoryName: 'Success'
16
+ }
17
+ ]
18
+ }
19
+ };
@@ -1,7 +1,34 @@
1
1
  import { COLORS, NodeConfig } from '../types';
2
+ import { Node } from '../../store/flow-definition';
2
3
 
3
4
  export const wait_for_response: NodeConfig = {
4
5
  type: 'wait_for_response',
5
6
  name: 'Wait for Response',
6
- color: COLORS.wait
7
+ color: COLORS.wait,
8
+ form: {
9
+ result_name: {
10
+ type: 'text',
11
+ label: 'Result Name',
12
+ helpText: 'The name to save the response as',
13
+ placeholder: 'response'
14
+ }
15
+ },
16
+ layout: ['timeout', 'result_name'],
17
+ toFormData: (node: Node) => {
18
+ return {
19
+ uuid: node.uuid,
20
+ result_name: node.router?.result_name || 'response'
21
+ };
22
+ },
23
+ fromFormData: (formData: any, originalNode: Node): Node => {
24
+ const router: any = {
25
+ ...originalNode.router,
26
+ result_name: formData.result_name || 'response'
27
+ };
28
+
29
+ return {
30
+ ...originalNode,
31
+ router
32
+ };
33
+ }
7
34
  };
package/src/flow/types.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { TemplateResult } from 'lit-html';
2
- import { Action } from '../store/flow-definition';
2
+ import { Action, Node } from '../store/flow-definition';
3
3
 
4
4
  export interface ValidationResult {
5
5
  valid: boolean;
@@ -65,39 +65,16 @@ export interface SliderAttributes {
65
65
  range?: boolean;
66
66
  }
67
67
 
68
- // Widget configuration using discriminated union for type safety
69
- export type WidgetConfig =
70
- | { type: 'temba-textinput'; attributes?: TextInputAttributes }
71
- | { type: 'temba-completion'; attributes?: CompletionAttributes }
72
- | { type: 'temba-select'; attributes?: SelectAttributes }
73
- | { type: 'temba-checkbox'; attributes?: CheckboxAttributes }
74
- | { type: 'temba-slider'; attributes?: SliderAttributes }
75
- | { type: string; attributes?: { [key: string]: any } }; // Generic fallback
68
+ export interface FormData extends Record<string, any> {}
76
69
 
77
- // Property configuration with the clean structure you want
78
- export interface PropertyConfig {
79
- // Form field metadata
80
- label?: string;
81
- helpText?: string;
82
- required?: boolean;
83
- maxLength?: number;
84
- minLength?: number;
85
- pattern?: string;
86
-
87
- // Widget configuration
88
- widget: WidgetConfig;
89
-
90
- // Conditional behavior based on other field values
91
- conditions?: {
92
- // When to show this field
93
- visible?: (formData: any) => boolean;
94
-
95
- // When this field is disabled
96
- disabled?: (formData: any) => boolean;
97
- };
70
+ export interface FormConfig {
71
+ form?: Record<string, FieldConfig>;
72
+ layout?: LayoutItem[];
73
+ sanitize?: (formData: FormData) => void;
74
+ validate?: (formData: FormData) => ValidationResult;
98
75
  }
99
76
 
100
- export interface NodeConfig {
77
+ export interface NodeConfig extends FormConfig {
101
78
  type: string;
102
79
  name?: string;
103
80
  color?: string;
@@ -108,22 +85,28 @@ export interface NodeConfig {
108
85
  operand?: string;
109
86
  configurable?: boolean; // can the rules be configured in the UI
110
87
  rules?: {
111
- type: 'has_number_between' | 'has_string' | 'has_value' | 'has_not_value';
88
+ type:
89
+ | 'has_number_between'
90
+ | 'has_string'
91
+ | 'has_value'
92
+ | 'has_not_value'
93
+ | 'has_text';
112
94
  arguments: string[];
113
95
  categoryName: string;
114
96
  }[];
115
97
  };
116
- properties?: { [key: string]: PropertyConfig };
117
- toFormData?: (node: any) => any;
118
- fromFormData?: (formData: any, originalNode: any) => any;
98
+
99
+ toFormData?: (node: Node) => FormData;
100
+ fromFormData?: (formData: FormData, originalNode: Node) => Node;
101
+ render?: (node: Node) => TemplateResult;
119
102
  }
120
103
 
121
104
  // New field configuration system for generic form generation
122
105
  export interface BaseFieldConfig {
123
106
  label?: string;
124
107
  required?: boolean;
125
- evaluated?: boolean; // if this field supports expression evaluation
126
- dependsOn?: string[]; // fields this field depends on
108
+ evaluated?: boolean;
109
+ dependsOn?: string[];
127
110
  computeValue?: (
128
111
  values: Record<string, any>,
129
112
  currentValue: any,
@@ -137,7 +120,7 @@ export interface BaseFieldConfig {
137
120
  helpText?: string;
138
121
 
139
122
  // Layout properties
140
- maxWidth?: string; // CSS max-width value (e.g., '200px', '50%', '10rem')
123
+ maxWidth?: string;
141
124
 
142
125
  // Conditional rendering
143
126
  conditions?: {
@@ -160,8 +143,9 @@ export interface TextareaFieldConfig extends BaseFieldConfig {
160
143
 
161
144
  export interface SelectFieldConfig extends BaseFieldConfig {
162
145
  type: 'select';
163
- options: string[] | { value: string; label: string }[];
146
+ options?: string[] | { value: string; label: string }[];
164
147
  multi?: boolean;
148
+ clearable?: boolean;
165
149
  searchable?: boolean;
166
150
  tags?: boolean;
167
151
  placeholder?: string;
@@ -170,6 +154,10 @@ export interface SelectFieldConfig extends BaseFieldConfig {
170
154
  nameKey?: string;
171
155
  endpoint?: string;
172
156
  emails?: boolean;
157
+ getName?: (item: any) => string;
158
+ flavor?: 'small' | 'large';
159
+ createArbitraryOption?: (input: string, options: any[]) => any;
160
+ allowCreate?: boolean;
173
161
  }
174
162
 
175
163
  export interface KeyValueFieldConfig extends BaseFieldConfig {
@@ -193,6 +181,7 @@ export interface ArrayFieldConfig extends BaseFieldConfig {
193
181
  value: any,
194
182
  allItems: any[]
195
183
  ) => any[];
184
+ isEmptyItem?: (item: any) => boolean;
196
185
  }
197
186
 
198
187
  export interface CheckboxFieldConfig extends BaseFieldConfig {
@@ -201,13 +190,27 @@ export interface CheckboxFieldConfig extends BaseFieldConfig {
201
190
  animateChange?: string;
202
191
  }
203
192
 
193
+ export interface MessageEditorFieldConfig extends BaseFieldConfig {
194
+ type: 'message-editor';
195
+ placeholder?: string;
196
+ minHeight?: number;
197
+ maxAttachments?: number;
198
+ accept?: string;
199
+ endpoint?: string;
200
+ counter?: string;
201
+ gsm?: boolean;
202
+ autogrow?: boolean;
203
+ disableCompletion?: boolean;
204
+ }
205
+
204
206
  export type FieldConfig =
205
207
  | TextFieldConfig
206
208
  | TextareaFieldConfig
207
209
  | SelectFieldConfig
208
210
  | KeyValueFieldConfig
209
211
  | ArrayFieldConfig
210
- | CheckboxFieldConfig;
212
+ | CheckboxFieldConfig
213
+ | MessageEditorFieldConfig;
211
214
 
212
215
  // Layout configurations for better form organization
213
216
  // Recursive layout system - any layout item can contain other layout items
@@ -228,8 +231,9 @@ export interface GroupLayoutConfig {
228
231
  label: string;
229
232
  items: LayoutItem[]; // can contain fields, rows, or other groups
230
233
  collapsible?: boolean;
231
- collapsed?: boolean; // initial state if collapsible
234
+ collapsed?: boolean | ((formData: any) => boolean); // initial state if collapsible - can be a function
232
235
  helpText?: string;
236
+ getGroupValueCount?: (formData: any) => number; // optional function to get count for bubble display
233
237
  }
234
238
 
235
239
  export type LayoutItem =
@@ -238,7 +242,7 @@ export type LayoutItem =
238
242
  | GroupLayoutConfig
239
243
  | string; // string is shorthand for field
240
244
 
241
- export interface ActionConfig {
245
+ export interface ActionConfig extends FormConfig {
242
246
  name: string;
243
247
  color: string;
244
248
  evaluated?: string[];
@@ -247,12 +251,8 @@ export interface ActionConfig {
247
251
  form?: Record<string, FieldConfig>;
248
252
  layout?: LayoutItem[]; // optional layout configuration - array of layout items
249
253
 
250
- // Action editor configuration (legacy)
251
- // Form-level transformations
252
254
  toFormData?: (action: Action) => any;
253
255
  fromFormData?: (formData: any) => Action;
254
-
255
- validate?: (action: Action) => ValidationResult;
256
256
  }
257
257
 
258
258
  export const COLORS = {
@@ -268,85 +268,3 @@ export const COLORS = {
268
268
  add: '#309c42',
269
269
  remove: '#e74c3c'
270
270
  };
271
-
272
- // Default property type mappings
273
- export function getDefaultComponent(value: any): WidgetConfig['type'] {
274
- if (typeof value === 'boolean') {
275
- return 'temba-checkbox';
276
- }
277
- if (typeof value === 'number') {
278
- return 'temba-textinput';
279
- }
280
- if (Array.isArray(value)) {
281
- return 'temba-select'; // For arrays, use multi-select
282
- }
283
- // Default to text input for strings and unknown types
284
- return 'temba-textinput';
285
- }
286
-
287
- // Get component properties for default mappings with proper typing
288
- export function getDefaultComponentProps(value: any): PropertyConfig {
289
- if (typeof value === 'boolean') {
290
- return {
291
- widget: { type: 'temba-checkbox' }
292
- };
293
- }
294
- if (typeof value === 'number') {
295
- return {
296
- widget: {
297
- type: 'temba-textinput',
298
- attributes: { type: 'number' }
299
- }
300
- };
301
- }
302
- if (Array.isArray(value)) {
303
- if (value.length > 0 && typeof value[0] === 'string') {
304
- return {
305
- widget: {
306
- type: 'temba-select',
307
- attributes: { multi: true, tags: true }
308
- }
309
- };
310
- }
311
- return {
312
- widget: {
313
- type: 'temba-select',
314
- attributes: { multi: true }
315
- }
316
- };
317
- }
318
- return {
319
- widget: { type: 'temba-textinput' }
320
- };
321
- }
322
-
323
- // Type guard functions for working with WidgetConfig
324
- export function isTextInputWidget(
325
- config: WidgetConfig
326
- ): config is { type: 'temba-textinput'; attributes?: TextInputAttributes } {
327
- return config.type === 'temba-textinput';
328
- }
329
-
330
- export function isCompletionWidget(
331
- config: WidgetConfig
332
- ): config is { type: 'temba-completion'; attributes?: CompletionAttributes } {
333
- return config.type === 'temba-completion';
334
- }
335
-
336
- export function isSelectWidget(
337
- config: WidgetConfig
338
- ): config is { type: 'temba-select'; attributes?: SelectAttributes } {
339
- return config.type === 'temba-select';
340
- }
341
-
342
- export function isCheckboxWidget(
343
- config: WidgetConfig
344
- ): config is { type: 'temba-checkbox'; attributes?: CheckboxAttributes } {
345
- return config.type === 'temba-checkbox';
346
- }
347
-
348
- export function isSliderWidget(
349
- config: WidgetConfig
350
- ): config is { type: 'slider'; attributes?: SliderAttributes } {
351
- return config.type === 'temba-slider';
352
- }