@nyaruka/temba-components 0.130.0 → 0.130.2

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 (252) hide show
  1. package/CHANGELOG.md +34 -4
  2. package/DEV_DATA.md +89 -0
  3. package/demo/data/flows/food-order.json +4 -4
  4. package/demo/data/flows/sample-flow.json +132 -147
  5. package/dist/temba-components.js +764 -628
  6. package/dist/temba-components.js.map +1 -1
  7. package/out-tsc/src/display/Chat.js +5 -3
  8. package/out-tsc/src/display/Chat.js.map +1 -1
  9. package/out-tsc/src/events.js.map +1 -1
  10. package/out-tsc/src/flow/CanvasNode.js +83 -78
  11. package/out-tsc/src/flow/CanvasNode.js.map +1 -1
  12. package/out-tsc/src/flow/Editor.js +1 -0
  13. package/out-tsc/src/flow/Editor.js.map +1 -1
  14. package/out-tsc/src/flow/NodeEditor.js +47 -3
  15. package/out-tsc/src/flow/NodeEditor.js.map +1 -1
  16. package/out-tsc/src/flow/actions/add_contact_groups.js +13 -1
  17. package/out-tsc/src/flow/actions/add_contact_groups.js.map +1 -1
  18. package/out-tsc/src/flow/actions/add_contact_urn.js +1 -1
  19. package/out-tsc/src/flow/actions/add_contact_urn.js.map +1 -1
  20. package/out-tsc/src/flow/actions/add_input_labels.js +1 -0
  21. package/out-tsc/src/flow/actions/add_input_labels.js.map +1 -1
  22. package/out-tsc/src/flow/actions/set_contact_channel.js +1 -1
  23. package/out-tsc/src/flow/actions/set_contact_channel.js.map +1 -1
  24. package/out-tsc/src/flow/actions/set_contact_field.js +2 -1
  25. package/out-tsc/src/flow/actions/set_contact_field.js.map +1 -1
  26. package/out-tsc/src/flow/actions/set_contact_language.js +3 -1
  27. package/out-tsc/src/flow/actions/set_contact_language.js.map +1 -1
  28. package/out-tsc/src/flow/actions/set_contact_name.js +1 -1
  29. package/out-tsc/src/flow/actions/set_contact_name.js.map +1 -1
  30. package/out-tsc/src/flow/actions/set_contact_status.js +17 -14
  31. package/out-tsc/src/flow/actions/set_contact_status.js.map +1 -1
  32. package/out-tsc/src/flow/actions/set_run_result.js +1 -1
  33. package/out-tsc/src/flow/actions/set_run_result.js.map +1 -1
  34. package/out-tsc/src/flow/nodes/split_by_llm.js +12 -12
  35. package/out-tsc/src/flow/nodes/split_by_llm.js.map +1 -1
  36. package/out-tsc/src/flow/nodes/wait_for_response.js +609 -6
  37. package/out-tsc/src/flow/nodes/wait_for_response.js.map +1 -1
  38. package/out-tsc/src/flow/operators.js +194 -0
  39. package/out-tsc/src/flow/operators.js.map +1 -0
  40. package/out-tsc/src/flow/types.js.map +1 -1
  41. package/out-tsc/src/form/ArrayEditor.js +84 -19
  42. package/out-tsc/src/form/ArrayEditor.js.map +1 -1
  43. package/out-tsc/src/form/Checkbox.js +12 -0
  44. package/out-tsc/src/form/Checkbox.js.map +1 -1
  45. package/out-tsc/src/form/FieldRenderer.js +13 -3
  46. package/out-tsc/src/form/FieldRenderer.js.map +1 -1
  47. package/out-tsc/src/form/TextInput.js +20 -1
  48. package/out-tsc/src/form/TextInput.js.map +1 -1
  49. package/out-tsc/src/form/select/Select.js +14 -1
  50. package/out-tsc/src/form/select/Select.js.map +1 -1
  51. package/out-tsc/src/interfaces.js.map +1 -1
  52. package/out-tsc/src/layout/Dialog.js +3 -4
  53. package/out-tsc/src/layout/Dialog.js.map +1 -1
  54. package/out-tsc/src/list/RunList.js +2 -2
  55. package/out-tsc/src/list/RunList.js.map +1 -1
  56. package/out-tsc/src/live/ContactChat.js +114 -34
  57. package/out-tsc/src/live/ContactChat.js.map +1 -1
  58. package/out-tsc/src/live/ContactDetails.js +7 -0
  59. package/out-tsc/src/live/ContactDetails.js.map +1 -1
  60. package/out-tsc/src/live/ContactNameFetch.js +1 -1
  61. package/out-tsc/src/live/ContactNameFetch.js.map +1 -1
  62. package/out-tsc/test/NodeHelper.js +25 -27
  63. package/out-tsc/test/NodeHelper.js.map +1 -1
  64. package/out-tsc/test/nodes/split_by_llm.test.js +12 -4
  65. package/out-tsc/test/nodes/split_by_llm.test.js.map +1 -1
  66. package/out-tsc/test/nodes/split_by_llm_categorize.test.js +101 -91
  67. package/out-tsc/test/nodes/split_by_llm_categorize.test.js.map +1 -1
  68. package/out-tsc/test/nodes/split_by_random.test.js +120 -112
  69. package/out-tsc/test/nodes/split_by_random.test.js.map +1 -1
  70. package/out-tsc/test/nodes/wait_for_digits.test.js +131 -111
  71. package/out-tsc/test/nodes/wait_for_digits.test.js.map +1 -1
  72. package/out-tsc/test/nodes/wait_for_response.test.js +549 -85
  73. package/out-tsc/test/nodes/wait_for_response.test.js.map +1 -1
  74. package/out-tsc/test/temba-checkbox.test.js +32 -32
  75. package/out-tsc/test/temba-checkbox.test.js.map +1 -1
  76. package/out-tsc/test/temba-contact-chat.test.js +2 -1
  77. package/out-tsc/test/temba-contact-chat.test.js.map +1 -1
  78. package/out-tsc/test/temba-dropdown.test.js +0 -4
  79. package/out-tsc/test/temba-dropdown.test.js.map +1 -1
  80. package/out-tsc/test/temba-flow-editor-node.test.js +9 -4
  81. package/out-tsc/test/temba-flow-editor-node.test.js.map +1 -1
  82. package/out-tsc/test/temba-integration-markdown.test.js +13 -15
  83. package/out-tsc/test/temba-integration-markdown.test.js.map +1 -1
  84. package/out-tsc/test/temba-node-editor.test.js +5 -38
  85. package/out-tsc/test/temba-node-editor.test.js.map +1 -1
  86. package/out-tsc/test/temba-run-list.test.js +2 -2
  87. package/out-tsc/test/temba-run-list.test.js.map +1 -1
  88. package/out-tsc/test/utils.test.js +2 -1
  89. package/out-tsc/test/utils.test.js.map +1 -1
  90. package/package.json +6 -2
  91. package/screenshots/truth/actions/add_contact_groups/editor/descriptive-group-names.png +0 -0
  92. package/screenshots/truth/actions/add_contact_groups/editor/long-group-names.png +0 -0
  93. package/screenshots/truth/actions/add_contact_groups/editor/many-groups.png +0 -0
  94. package/screenshots/truth/actions/add_contact_groups/editor/multiple-groups.png +0 -0
  95. package/screenshots/truth/actions/add_contact_groups/editor/single-group.png +0 -0
  96. package/screenshots/truth/actions/add_contact_groups/render/descriptive-group-names.png +0 -0
  97. package/screenshots/truth/actions/add_contact_groups/render/long-group-names.png +0 -0
  98. package/screenshots/truth/actions/add_contact_groups/render/many-groups.png +0 -0
  99. package/screenshots/truth/actions/add_contact_groups/render/multiple-groups.png +0 -0
  100. package/screenshots/truth/actions/add_contact_groups/render/single-group.png +0 -0
  101. package/screenshots/truth/actions/remove_contact_groups/editor/cleanup-groups.png +0 -0
  102. package/screenshots/truth/actions/remove_contact_groups/editor/long-descriptive-group-names.png +0 -0
  103. package/screenshots/truth/actions/remove_contact_groups/editor/many-groups.png +0 -0
  104. package/screenshots/truth/actions/remove_contact_groups/editor/multiple-groups.png +0 -0
  105. package/screenshots/truth/actions/remove_contact_groups/editor/remove-from-all-groups.png +0 -0
  106. package/screenshots/truth/actions/remove_contact_groups/editor/single-group.png +0 -0
  107. package/screenshots/truth/actions/remove_contact_groups/render/cleanup-groups.png +0 -0
  108. package/screenshots/truth/actions/remove_contact_groups/render/long-descriptive-group-names.png +0 -0
  109. package/screenshots/truth/actions/remove_contact_groups/render/many-groups.png +0 -0
  110. package/screenshots/truth/actions/remove_contact_groups/render/multiple-groups.png +0 -0
  111. package/screenshots/truth/actions/remove_contact_groups/render/remove-from-all-groups.png +0 -0
  112. package/screenshots/truth/actions/remove_contact_groups/render/single-group.png +0 -0
  113. package/screenshots/truth/actions/send_email/editor/complex-business-email.png +0 -0
  114. package/screenshots/truth/actions/send_email/editor/empty-body.png +0 -0
  115. package/screenshots/truth/actions/send_email/editor/empty-subject.png +0 -0
  116. package/screenshots/truth/actions/send_email/editor/long-subject.png +0 -0
  117. package/screenshots/truth/actions/send_email/editor/multiline-body.png +0 -0
  118. package/screenshots/truth/actions/send_email/editor/multiple-recipients.png +0 -0
  119. package/screenshots/truth/actions/send_email/editor/simple-email.png +0 -0
  120. package/screenshots/truth/actions/send_email/editor/with-expressions.png +0 -0
  121. package/screenshots/truth/actions/send_email/render/complex-business-email.png +0 -0
  122. package/screenshots/truth/actions/send_email/render/empty-body.png +0 -0
  123. package/screenshots/truth/actions/send_email/render/empty-subject.png +0 -0
  124. package/screenshots/truth/actions/send_email/render/long-subject.png +0 -0
  125. package/screenshots/truth/actions/send_email/render/multiline-body.png +0 -0
  126. package/screenshots/truth/actions/send_email/render/multiple-recipients.png +0 -0
  127. package/screenshots/truth/actions/send_email/render/simple-email.png +0 -0
  128. package/screenshots/truth/actions/send_email/render/with-expressions.png +0 -0
  129. package/screenshots/truth/actions/send_msg/editor/long-quick-replies.png +0 -0
  130. package/screenshots/truth/actions/send_msg/editor/multiline-text-with-replies.png +0 -0
  131. package/screenshots/truth/actions/send_msg/editor/simple-text.png +0 -0
  132. package/screenshots/truth/actions/send_msg/editor/text-with-linebreaks.png +0 -0
  133. package/screenshots/truth/actions/send_msg/editor/text-with-many-quick-replies.png +0 -0
  134. package/screenshots/truth/actions/send_msg/editor/text-with-quick-replies.png +0 -0
  135. package/screenshots/truth/actions/send_msg/editor/text-without-quick-replies.png +0 -0
  136. package/screenshots/truth/actions/send_msg/render/long-quick-replies.png +0 -0
  137. package/screenshots/truth/actions/send_msg/render/multiline-text-with-replies.png +0 -0
  138. package/screenshots/truth/actions/send_msg/render/simple-text.png +0 -0
  139. package/screenshots/truth/actions/send_msg/render/text-with-linebreaks.png +0 -0
  140. package/screenshots/truth/actions/send_msg/render/text-with-many-quick-replies.png +0 -0
  141. package/screenshots/truth/actions/send_msg/render/text-with-quick-replies.png +0 -0
  142. package/screenshots/truth/actions/send_msg/render/text-without-quick-replies.png +0 -0
  143. package/screenshots/truth/checkbox/checkbox-label-background-hover.png +0 -0
  144. package/screenshots/truth/checkbox/checkbox-whitespace-label-no-background-hover.png +0 -0
  145. package/screenshots/truth/checkbox/checkbox-with-help-text.png +0 -0
  146. package/screenshots/truth/checkbox/checked.png +0 -0
  147. package/screenshots/truth/checkbox/default.png +0 -0
  148. package/screenshots/truth/editor/wait.png +0 -0
  149. package/screenshots/truth/integration/textinput-markdown-errors.png +0 -0
  150. package/screenshots/truth/lightbox/img-zoomed.png +0 -0
  151. package/screenshots/truth/nodes/split_by_llm/editor/information-extraction.png +0 -0
  152. package/screenshots/truth/nodes/split_by_llm/editor/sentiment-analysis.png +0 -0
  153. package/screenshots/truth/nodes/split_by_llm/editor/summarization.png +0 -0
  154. package/screenshots/truth/nodes/split_by_llm/editor/translation-task.png +0 -0
  155. package/screenshots/truth/nodes/split_by_llm/render/information-extraction.png +0 -0
  156. package/screenshots/truth/nodes/split_by_llm/render/sentiment-analysis.png +0 -0
  157. package/screenshots/truth/nodes/split_by_llm/render/summarization.png +0 -0
  158. package/screenshots/truth/nodes/split_by_llm/render/translation-task.png +0 -0
  159. package/screenshots/truth/nodes/split_by_llm_categorize/editor/basic-categorization.png +0 -0
  160. package/screenshots/truth/nodes/split_by_llm_categorize/editor/custom-input-and-result-name.png +0 -0
  161. package/screenshots/truth/nodes/split_by_llm_categorize/editor/feedback-categorization.png +0 -0
  162. package/screenshots/truth/nodes/split_by_llm_categorize/editor/many-categories.png +0 -0
  163. package/screenshots/truth/nodes/split_by_llm_categorize/editor/minimal-categories.png +0 -0
  164. package/screenshots/truth/nodes/split_by_llm_categorize/render/basic-categorization.png +0 -0
  165. package/screenshots/truth/nodes/split_by_llm_categorize/render/custom-input-and-result-name.png +0 -0
  166. package/screenshots/truth/nodes/split_by_llm_categorize/render/feedback-categorization.png +0 -0
  167. package/screenshots/truth/nodes/split_by_llm_categorize/render/many-categories.png +0 -0
  168. package/screenshots/truth/nodes/split_by_llm_categorize/render/minimal-categories.png +0 -0
  169. package/screenshots/truth/nodes/split_by_random/editor/ab-test-multiple-variants.png +0 -0
  170. package/screenshots/truth/nodes/split_by_random/editor/sampling-split.png +0 -0
  171. package/screenshots/truth/nodes/split_by_random/editor/three-way-split.png +0 -0
  172. package/screenshots/truth/nodes/split_by_random/editor/two-bucket-split.png +0 -0
  173. package/screenshots/truth/nodes/split_by_random/render/ab-test-multiple-variants.png +0 -0
  174. package/screenshots/truth/nodes/split_by_random/render/sampling-split.png +0 -0
  175. package/screenshots/truth/nodes/split_by_random/render/three-way-split.png +0 -0
  176. package/screenshots/truth/nodes/split_by_random/render/two-bucket-split.png +0 -0
  177. package/screenshots/truth/nodes/wait_for_digits/editor/basic-digits-wait.png +0 -0
  178. package/screenshots/truth/nodes/wait_for_digits/editor/phone-number-collection.png +0 -0
  179. package/screenshots/truth/nodes/wait_for_digits/editor/single-digit-with-timeout.png +0 -0
  180. package/screenshots/truth/nodes/wait_for_digits/editor/verification-code.png +0 -0
  181. package/screenshots/truth/nodes/wait_for_digits/render/basic-digits-wait.png +0 -0
  182. package/screenshots/truth/nodes/wait_for_digits/render/phone-number-collection.png +0 -0
  183. package/screenshots/truth/nodes/wait_for_digits/render/single-digit-with-timeout.png +0 -0
  184. package/screenshots/truth/nodes/wait_for_digits/render/verification-code.png +0 -0
  185. package/screenshots/truth/nodes/wait_for_response/editor/basic-wait.png +0 -0
  186. package/screenshots/truth/nodes/wait_for_response/editor/custom-result-name.png +0 -0
  187. package/screenshots/truth/nodes/wait_for_response/editor/no-timeout.png +0 -0
  188. package/screenshots/truth/nodes/wait_for_response/editor/short-timeout.png +0 -0
  189. package/screenshots/truth/nodes/wait_for_response/render/basic-wait.png +0 -0
  190. package/screenshots/truth/nodes/wait_for_response/render/custom-result-name.png +0 -0
  191. package/screenshots/truth/nodes/wait_for_response/render/no-timeout.png +0 -0
  192. package/screenshots/truth/nodes/wait_for_response/render/short-timeout.png +0 -0
  193. package/screenshots/truth/run-list/basic.png +0 -0
  194. package/screenshots/truth/templates/default.png +0 -0
  195. package/screenshots/truth/wait-for-response/rules-editor.png +0 -0
  196. package/screenshots/truth/wait-for-response/timeout-editor-unchecked.png +0 -0
  197. package/screenshots/truth/wait-for-response/timeout-editor.png +0 -0
  198. package/scripts/dev-data-sync.mjs +182 -0
  199. package/src/display/Chat.ts +6 -4
  200. package/src/events.ts +6 -5
  201. package/src/flow/CanvasNode.ts +89 -79
  202. package/src/flow/Editor.ts +1 -0
  203. package/src/flow/NodeEditor.ts +55 -3
  204. package/src/flow/actions/add_contact_groups.ts +16 -1
  205. package/src/flow/actions/add_contact_urn.ts +1 -1
  206. package/src/flow/actions/add_input_labels.ts +1 -0
  207. package/src/flow/actions/set_contact_channel.ts +1 -1
  208. package/src/flow/actions/set_contact_field.ts +2 -1
  209. package/src/flow/actions/set_contact_language.ts +3 -1
  210. package/src/flow/actions/set_contact_name.ts +1 -1
  211. package/src/flow/actions/set_contact_status.ts +18 -18
  212. package/src/flow/actions/set_run_result.ts +1 -1
  213. package/src/flow/nodes/split_by_llm.ts +14 -13
  214. package/src/flow/nodes/wait_for_response.ts +717 -5
  215. package/src/flow/operators.ts +215 -0
  216. package/src/flow/types.ts +10 -2
  217. package/src/form/ArrayEditor.ts +117 -37
  218. package/src/form/Checkbox.ts +12 -0
  219. package/src/form/FieldRenderer.ts +24 -3
  220. package/src/form/TextInput.ts +19 -1
  221. package/src/form/select/Select.ts +15 -4
  222. package/src/interfaces.ts +1 -1
  223. package/src/layout/Dialog.ts +4 -4
  224. package/src/list/RunList.ts +2 -2
  225. package/src/live/ContactChat.ts +144 -58
  226. package/src/live/ContactDetails.ts +7 -0
  227. package/src/live/ContactNameFetch.ts +1 -1
  228. package/static/api/labels.json +6 -1
  229. package/test/NodeHelper.ts +38 -40
  230. package/test/nodes/split_by_llm.test.ts +43 -32
  231. package/test/nodes/split_by_llm_categorize.test.ts +130 -120
  232. package/test/nodes/split_by_random.test.ts +136 -128
  233. package/test/nodes/wait_for_digits.test.ts +147 -127
  234. package/test/nodes/wait_for_response.test.ts +657 -104
  235. package/test/temba-checkbox.test.ts +36 -32
  236. package/test/temba-contact-chat.test.ts +2 -1
  237. package/test/temba-dropdown.test.ts +0 -12
  238. package/test/temba-flow-editor-node.test.ts +11 -4
  239. package/test/temba-integration-markdown.test.ts +16 -17
  240. package/test/temba-node-editor.test.ts +5 -43
  241. package/test/temba-run-list.test.ts +2 -2
  242. package/test/utils.test.ts +2 -1
  243. package/test-assets/list/runs.json +8 -8
  244. package/web-dev-mock.mjs +86 -30
  245. package/web-dev-server.config.mjs +272 -31
  246. package/screenshots/truth/dropdown/bottom-edge-collision.png +0 -0
  247. package/screenshots/truth/dropdown/right-edge-collision.png +0 -0
  248. package/screenshots/truth/editor/send_msg.png +0 -0
  249. package/screenshots/truth/editor/set_contact_language.png +0 -0
  250. package/screenshots/truth/editor/set_contact_name.png +0 -0
  251. package/screenshots/truth/editor/set_run_result.png +0 -0
  252. package/screenshots/truth/integration/checkbox-markdown-errors.png +0 -0
@@ -1,11 +1,377 @@
1
1
  import { COLORS, NodeConfig } from '../types';
2
- import { Node } from '../../store/flow-definition';
2
+ import { Node, Category, Exit, Case } from '../../store/flow-definition';
3
+ import { generateUUID } from '../../utils';
4
+ import {
5
+ getWaitForResponseOperators,
6
+ operatorsToSelectOptions,
7
+ getOperatorConfig
8
+ } from '../operators';
9
+
10
+ const TIMEOUT_OPTIONS = [
11
+ { value: '60', name: '1 minute' },
12
+ { value: '120', name: '2 minutes' },
13
+ { value: '180', name: '3 minutes' },
14
+ { value: '240', name: '4 minutes' },
15
+ { value: '300', name: '5 minutes' },
16
+ { value: '600', name: '10 minutes' },
17
+ { value: '900', name: '15 minutes' },
18
+ { value: '1800', name: '30 minutes' },
19
+ { value: '3600', name: '1 hour' },
20
+ { value: '7200', name: '2 hours' },
21
+ { value: '10800', name: '3 hours' },
22
+ { value: '21600', name: '6 hours' },
23
+ { value: '43200', name: '12 hours' },
24
+ { value: '64800', name: '18 hours' },
25
+ { value: '86400', name: '1 day' },
26
+ { value: '172800', name: '2 days' },
27
+ { value: '259200', name: '3 days' },
28
+ { value: '604800', name: '1 week' }
29
+ ];
30
+
31
+ // Helper function to create a wait_for_response router with user rules
32
+ const createWaitForResponseRouter = (
33
+ userRules: any[],
34
+ existingCategories: Category[] = [],
35
+ existingExits: Exit[] = [],
36
+ existingCases: Case[] = []
37
+ ) => {
38
+ const categories: Category[] = [];
39
+ const exits: Exit[] = [];
40
+ const cases: Case[] = [];
41
+
42
+ // Filter existing categories to get only user-defined rules (exclude system categories)
43
+ const existingUserCategories = existingCategories.filter(
44
+ (cat) =>
45
+ cat.name !== 'No Response' &&
46
+ cat.name !== 'Other' &&
47
+ cat.name !== 'Timeout'
48
+ );
49
+
50
+ // Group rules by category name (case-insensitive) to merge them
51
+ const rulesByCategory = new Map<string, any[]>();
52
+ userRules.forEach((rule) => {
53
+ const categoryKey = rule.category.trim().toLowerCase();
54
+ if (!rulesByCategory.has(categoryKey)) {
55
+ rulesByCategory.set(categoryKey, []);
56
+ }
57
+ rulesByCategory.get(categoryKey)!.push(rule);
58
+ });
59
+
60
+ // Track category creation order to preserve UUID mapping
61
+ const categoryOrder: string[] = [];
62
+ userRules.forEach((rule) => {
63
+ const categoryKey = rule.category.trim().toLowerCase();
64
+ if (!categoryOrder.includes(categoryKey)) {
65
+ categoryOrder.push(categoryKey);
66
+ }
67
+ });
68
+
69
+ // Create categories, exits, and cases for each unique category
70
+ categoryOrder.forEach((categoryKey, categoryIndex) => {
71
+ const rulesForCategory = rulesByCategory.get(categoryKey)!;
72
+ const categoryName = rulesForCategory[0].category.trim(); // Use the first occurrence's casing
73
+
74
+ // Try to find existing category by position/index to preserve UUIDs when names change
75
+ const existingCategory = existingUserCategories[categoryIndex];
76
+ const existingExit = existingCategory
77
+ ? existingExits.find((exit) => exit.uuid === existingCategory.exit_uuid)
78
+ : null;
79
+
80
+ const exitUuid = existingExit?.uuid || generateUUID();
81
+ const categoryUuid = existingCategory?.uuid || generateUUID();
82
+
83
+ // Create single category for all rules with this category name
84
+ categories.push({
85
+ uuid: categoryUuid,
86
+ name: categoryName,
87
+ exit_uuid: exitUuid
88
+ });
89
+
90
+ exits.push({
91
+ uuid: exitUuid,
92
+ destination_uuid: existingExit?.destination_uuid || null
93
+ });
94
+
95
+ // Create a case for each rule in this category
96
+ rulesForCategory.forEach((rule) => {
97
+ // Try to find existing case for this rule by looking at the original rule order
98
+ const originalRuleIndex = userRules.findIndex((r) => r === rule);
99
+ const existingCase = existingCases[originalRuleIndex];
100
+
101
+ const caseUuid = existingCase?.uuid || generateUUID();
102
+
103
+ // Parse rule value based on operator configuration
104
+ const operatorConfig = getOperatorConfig(rule.operator);
105
+ let arguments_: string[] = [];
106
+
107
+ if (operatorConfig) {
108
+ if (operatorConfig.operands === 0) {
109
+ // No operands needed
110
+ arguments_ = [];
111
+ } else if (operatorConfig.operands === 2) {
112
+ // Split value for two operands (e.g., "1 10" for between)
113
+ arguments_ = rule.value
114
+ .split(' ')
115
+ .filter((arg: string) => arg.trim());
116
+ } else {
117
+ // Single operand - but split words for operators that expect multiple words
118
+ if (rule.value && rule.value.trim()) {
119
+ // Split on spaces and filter out empty strings
120
+ arguments_ = rule.value
121
+ .trim()
122
+ .split(/\s+/)
123
+ .filter((arg: string) => arg.length > 0);
124
+ } else {
125
+ arguments_ = [];
126
+ }
127
+ }
128
+ } else {
129
+ // Fallback for unknown operators - split on spaces if value exists
130
+ if (rule.value && rule.value.trim()) {
131
+ arguments_ = rule.value
132
+ .trim()
133
+ .split(/\s+/)
134
+ .filter((arg: string) => arg.length > 0);
135
+ } else {
136
+ arguments_ = [];
137
+ }
138
+ }
139
+
140
+ cases.push({
141
+ uuid: caseUuid,
142
+ type: rule.operator,
143
+ arguments: arguments_,
144
+ category_uuid: categoryUuid
145
+ });
146
+ });
147
+ });
148
+
149
+ // Preserve existing timeout categories like "No Response"
150
+ existingCategories.forEach((category) => {
151
+ if (category.name === 'No Response' || category.name === 'Timeout') {
152
+ const existingExit = existingExits.find(
153
+ (exit) => exit.uuid === category.exit_uuid
154
+ );
155
+
156
+ if (existingExit) {
157
+ categories.push(category);
158
+ exits.push(existingExit);
159
+ }
160
+ }
161
+ });
162
+
163
+ // Add "Other" category (default) only if there are user rules
164
+ if (userRules.length > 0) {
165
+ const existingOtherCategory = existingCategories.find(
166
+ (cat) => cat.name === 'Other'
167
+ );
168
+ const existingOtherExit = existingOtherCategory
169
+ ? existingExits.find(
170
+ (exit) => exit.uuid === existingOtherCategory.exit_uuid
171
+ )
172
+ : null;
173
+
174
+ const otherExitUuid = existingOtherExit?.uuid || generateUUID();
175
+ const otherCategoryUuid = existingOtherCategory?.uuid || generateUUID();
176
+
177
+ categories.push({
178
+ uuid: otherCategoryUuid,
179
+ name: 'Other',
180
+ exit_uuid: otherExitUuid
181
+ });
182
+
183
+ exits.push({
184
+ uuid: otherExitUuid,
185
+ destination_uuid: existingOtherExit?.destination_uuid || null
186
+ });
187
+ }
188
+
189
+ return {
190
+ router: {
191
+ type: 'switch' as const,
192
+ categories: categories,
193
+ default_category_uuid: categories.find((cat) => cat.name === 'Other')
194
+ ?.uuid,
195
+ operand: '@input.text',
196
+ cases: cases
197
+ },
198
+ exits: exits
199
+ };
200
+ };
3
201
 
4
202
  export const wait_for_response: NodeConfig = {
5
203
  type: 'wait_for_response',
6
204
  name: 'Wait for Response',
7
205
  color: COLORS.wait,
206
+ dialogSize: 'large',
8
207
  form: {
208
+ rules: {
209
+ type: 'array',
210
+ helpText: 'Define rules to categorize responses',
211
+ itemLabel: 'Rule',
212
+ minItems: 0,
213
+ maxItems: 100,
214
+ maintainEmptyItem: true, // Explicitly enable empty item maintenance
215
+ isEmptyItem: (item: any) => {
216
+ // Helper function to get operator value from various formats
217
+ const getOperatorValue = (operator: any): string => {
218
+ if (typeof operator === 'string') {
219
+ return operator.trim();
220
+ } else if (Array.isArray(operator) && operator.length > 0) {
221
+ // Handle array format: [{value: "has_any_word", name: "..."}]
222
+ const firstOperator = operator[0];
223
+ if (
224
+ firstOperator &&
225
+ typeof firstOperator === 'object' &&
226
+ firstOperator.value
227
+ ) {
228
+ return firstOperator.value.trim();
229
+ }
230
+ } else if (
231
+ operator &&
232
+ typeof operator === 'object' &&
233
+ operator.value
234
+ ) {
235
+ // Handle object format: {value: "has_any_word", name: "..."}
236
+ return operator.value.trim();
237
+ }
238
+ return '';
239
+ };
240
+
241
+ // Check if operator and category are provided
242
+ const operatorValue = getOperatorValue(item.operator);
243
+ if (!operatorValue || !item.category || item.category.trim() === '') {
244
+ return true;
245
+ }
246
+
247
+ // Check if value is required based on operator configuration
248
+ const operatorConfig = getOperatorConfig(operatorValue);
249
+ if (operatorConfig && operatorConfig.operands === 1) {
250
+ // value1 is required for this operator
251
+ return !item.value1 || item.value1.trim() === '';
252
+ } else if (operatorConfig && operatorConfig.operands === 2) {
253
+ // Both value1 and value2 are required for this operator
254
+ return (
255
+ !item.value1 ||
256
+ item.value1.trim() === '' ||
257
+ !item.value2 ||
258
+ item.value2.trim() === ''
259
+ );
260
+ }
261
+
262
+ // No value required for this operator
263
+ return false;
264
+ },
265
+ itemConfig: {
266
+ operator: {
267
+ type: 'select',
268
+ required: true,
269
+ multi: false, // Explicitly set as single-select
270
+ options: operatorsToSelectOptions(getWaitForResponseOperators()),
271
+ flavor: 'xsmall',
272
+ width: '200px'
273
+ },
274
+ value1: {
275
+ type: 'text',
276
+ flavor: 'xsmall',
277
+ conditions: {
278
+ visible: (formData: Record<string, any>) => {
279
+ // Helper function to get operator value from various formats
280
+ const getOperatorValue = (operator: any): string => {
281
+ if (typeof operator === 'string') {
282
+ return operator.trim();
283
+ } else if (Array.isArray(operator) && operator.length > 0) {
284
+ const firstOperator = operator[0];
285
+ if (
286
+ firstOperator &&
287
+ typeof firstOperator === 'object' &&
288
+ firstOperator.value
289
+ ) {
290
+ return firstOperator.value.trim();
291
+ }
292
+ } else if (
293
+ operator &&
294
+ typeof operator === 'object' &&
295
+ operator.value
296
+ ) {
297
+ return operator.value.trim();
298
+ }
299
+ return '';
300
+ };
301
+
302
+ // Show value1 field for operators that require 1 or 2 operands
303
+ const operatorValue = getOperatorValue(formData.operator);
304
+ const operatorConfig = getOperatorConfig(operatorValue);
305
+ return operatorConfig ? operatorConfig.operands >= 1 : true;
306
+ }
307
+ }
308
+ },
309
+ value2: {
310
+ type: 'text',
311
+ flavor: 'xsmall',
312
+ conditions: {
313
+ visible: (formData: Record<string, any>) => {
314
+ // Helper function to get operator value from various formats
315
+ const getOperatorValue = (operator: any): string => {
316
+ if (typeof operator === 'string') {
317
+ return operator.trim();
318
+ } else if (Array.isArray(operator) && operator.length > 0) {
319
+ const firstOperator = operator[0];
320
+ if (
321
+ firstOperator &&
322
+ typeof firstOperator === 'object' &&
323
+ firstOperator.value
324
+ ) {
325
+ return firstOperator.value.trim();
326
+ }
327
+ } else if (
328
+ operator &&
329
+ typeof operator === 'object' &&
330
+ operator.value
331
+ ) {
332
+ return operator.value.trim();
333
+ }
334
+ return '';
335
+ };
336
+
337
+ // Show value2 field only if operator requires exactly 2 operands
338
+ const operatorValue = getOperatorValue(formData.operator);
339
+ const operatorConfig = getOperatorConfig(operatorValue);
340
+ return operatorConfig ? operatorConfig.operands === 2 : false;
341
+ }
342
+ }
343
+ },
344
+ category: {
345
+ type: 'text',
346
+ placeholder: 'Category',
347
+ required: true,
348
+ maxWidth: '120px',
349
+ flavor: 'xsmall'
350
+ }
351
+ }
352
+ },
353
+ timeout_enabled: {
354
+ type: 'checkbox',
355
+ label: (formData: Record<string, any>) => {
356
+ return formData.timeout_enabled
357
+ ? 'Continue when there is no response for'
358
+ : 'Continue when there is no response..';
359
+ },
360
+ labelPadding: '4px 8px'
361
+ },
362
+ timeout_duration: {
363
+ type: 'select',
364
+ placeholder: '5 minutes',
365
+ multi: false,
366
+ maxWidth: '150px',
367
+ flavor: 'xsmall',
368
+ options: TIMEOUT_OPTIONS,
369
+ conditions: {
370
+ visible: (formData: Record<string, any>) => {
371
+ return formData.timeout_enabled === true;
372
+ }
373
+ }
374
+ },
9
375
  result_name: {
10
376
  type: 'text',
11
377
  label: 'Result Name',
@@ -13,22 +379,368 @@ export const wait_for_response: NodeConfig = {
13
379
  placeholder: 'response'
14
380
  }
15
381
  },
16
- layout: ['timeout', 'result_name'],
382
+ layout: ['rules', 'result_name'],
383
+ gutter: [
384
+ {
385
+ type: 'row',
386
+ items: ['timeout_enabled', 'timeout_duration'],
387
+ gap: '0.5rem'
388
+ }
389
+ ],
390
+ validate: (_formData: any) => {
391
+ const errors: { [key: string]: string } = {};
392
+
393
+ // No validation needed - allow multiple rules to use same category name
394
+ // Rules with the same category name will be merged to use the same exit
395
+
396
+ return {
397
+ valid: Object.keys(errors).length === 0,
398
+ errors
399
+ };
400
+ },
17
401
  toFormData: (node: Node) => {
402
+ // Extract rules from router cases
403
+ const rules = [];
404
+ if (node.router?.cases && node.router?.categories) {
405
+ node.router.cases.forEach((case_) => {
406
+ // Find the category for this case
407
+ const category = node.router!.categories.find(
408
+ (cat) => cat.uuid === case_.category_uuid
409
+ );
410
+
411
+ // Skip timeout/system categories like "No Response"
412
+ if (
413
+ category &&
414
+ category.name !== 'No Response' &&
415
+ category.name !== 'Other'
416
+ ) {
417
+ // Handle different operator types
418
+ const operatorConfig = getOperatorConfig(case_.type);
419
+ const operatorDisplayName = operatorConfig
420
+ ? operatorConfig.name
421
+ : case_.type;
422
+ let value1 = '';
423
+ let value2 = '';
424
+
425
+ if (operatorConfig && operatorConfig.operands === 0) {
426
+ // No value needed for operators like has_text, has_number
427
+ value1 = '';
428
+ value2 = '';
429
+ } else if (operatorConfig && operatorConfig.operands === 1) {
430
+ // Single value for operators like has_number_lt - use value1
431
+ value1 = case_.arguments.join(' ');
432
+ value2 = '';
433
+ } else if (operatorConfig && operatorConfig.operands === 2) {
434
+ // Two separate values for operators like has_number_between
435
+ value1 = case_.arguments[0] || '';
436
+ value2 = case_.arguments[1] || '';
437
+ } else {
438
+ // Fallback: use first argument for unknown operators
439
+ value1 = case_.arguments.join(' ');
440
+ value2 = '';
441
+ }
442
+
443
+ rules.push({
444
+ operator: { value: case_.type, name: operatorDisplayName },
445
+ value1: value1,
446
+ value2: value2,
447
+ category: category.name
448
+ });
449
+ }
450
+ });
451
+ }
452
+
453
+ // Extract timeout configuration
454
+ const timeoutSeconds = node.router?.wait?.timeout?.seconds;
455
+ let timeoutOption = TIMEOUT_OPTIONS.find(
456
+ (opt) => opt.value === String(timeoutSeconds)
457
+ );
458
+
459
+ if (!timeoutOption) {
460
+ timeoutOption = { value: '300', name: '5 minutes' };
461
+ }
462
+
18
463
  return {
19
464
  uuid: node.uuid,
465
+ rules: rules,
466
+ timeout_enabled: !!timeoutSeconds,
467
+ timeout_duration: timeoutOption,
20
468
  result_name: node.router?.result_name || 'response'
21
469
  };
22
470
  },
23
471
  fromFormData: (formData: any, originalNode: Node): Node => {
24
- const router: any = {
25
- ...originalNode.router,
472
+ // Helper function to get operator value from various formats
473
+ const getOperatorValue = (operator: any): string => {
474
+ if (typeof operator === 'string') {
475
+ return operator.trim();
476
+ } else if (Array.isArray(operator) && operator.length > 0) {
477
+ // Handle array format: [{value: "has_any_word", name: "..."}]
478
+ const firstOperator = operator[0];
479
+ if (
480
+ firstOperator &&
481
+ typeof firstOperator === 'object' &&
482
+ firstOperator.value
483
+ ) {
484
+ return firstOperator.value.trim();
485
+ }
486
+ } else if (operator && typeof operator === 'object' && operator.value) {
487
+ // Handle object format: {value: "has_any_word", name: "..."}
488
+ return operator.value.trim();
489
+ }
490
+ return '';
491
+ };
492
+
493
+ // Get user rules
494
+ const userRules = (formData.rules || [])
495
+ .filter((rule: any) => {
496
+ // Always need operator and category
497
+ const operatorValue = getOperatorValue(rule?.operator);
498
+ if (
499
+ !operatorValue ||
500
+ !rule?.category ||
501
+ operatorValue === '' ||
502
+ rule.category.trim() === ''
503
+ ) {
504
+ return false;
505
+ }
506
+
507
+ // Check if value is required based on operator
508
+ const operatorConfig = getOperatorConfig(operatorValue);
509
+ if (operatorConfig && operatorConfig.operands === 1) {
510
+ // value1 is required for this operator
511
+ return rule?.value1 && rule.value1.trim() !== '';
512
+ } else if (operatorConfig && operatorConfig.operands === 2) {
513
+ // Both value1 and value2 are required for this operator
514
+ return (
515
+ rule?.value1 &&
516
+ rule.value1.trim() !== '' &&
517
+ rule?.value2 &&
518
+ rule.value2.trim() !== ''
519
+ );
520
+ }
521
+
522
+ // No value required for this operator
523
+ return true;
524
+ })
525
+ .map((rule: any) => {
526
+ const operatorValue = getOperatorValue(rule.operator);
527
+ const operatorConfig = getOperatorConfig(operatorValue);
528
+
529
+ let value = '';
530
+
531
+ if (operatorConfig && operatorConfig.operands === 1) {
532
+ // Single value from value1
533
+ value = rule.value1 ? rule.value1.trim() : '';
534
+ } else if (operatorConfig && operatorConfig.operands === 2) {
535
+ // Two values - combine them with space
536
+ const val1 = rule.value1 ? rule.value1.trim() : '';
537
+ const val2 = rule.value2 ? rule.value2.trim() : '';
538
+ value = `${val1} ${val2}`.trim();
539
+ } else {
540
+ // No value needed for 0-operand operators
541
+ value = '';
542
+ }
543
+
544
+ return {
545
+ operator: operatorValue,
546
+ value: value,
547
+ category: rule.category.trim()
548
+ };
549
+ });
550
+
551
+ // If no user rules, clear cases but preserve other router config
552
+ if (userRules.length === 0) {
553
+ const router: any = {
554
+ ...originalNode.router,
555
+ result_name: formData.result_name || 'response'
556
+ };
557
+
558
+ // Only set cases to empty if the original node had cases
559
+ if (originalNode.router?.cases !== undefined) {
560
+ router.cases = []; // Clear all cases when no rules
561
+ }
562
+
563
+ // Build wait configuration based on form data
564
+ const waitConfig: any = {
565
+ type: 'msg'
566
+ };
567
+
568
+ // Add timeout if enabled
569
+ if (formData.timeout_enabled) {
570
+ // Extract timeout value (handle both string and object formats)
571
+ let timeoutSeconds;
572
+
573
+ if (formData.timeout_duration) {
574
+ if (
575
+ Array.isArray(formData.timeout_duration) &&
576
+ formData.timeout_duration.length > 0
577
+ ) {
578
+ // Handle array of selected options (multi-select behavior)
579
+ timeoutSeconds = parseInt(formData.timeout_duration[0].value, 10);
580
+ } else if (typeof formData.timeout_duration === 'string') {
581
+ timeoutSeconds = parseInt(formData.timeout_duration, 10);
582
+ } else if (
583
+ formData.timeout_duration &&
584
+ typeof formData.timeout_duration === 'object' &&
585
+ formData.timeout_duration.value
586
+ ) {
587
+ timeoutSeconds = parseInt(formData.timeout_duration.value, 10);
588
+ } else {
589
+ timeoutSeconds = 300; // Default to 5 minutes
590
+ }
591
+ } else {
592
+ // No duration selected, use default
593
+ timeoutSeconds = 300; // Default to 5 minutes
594
+ }
595
+
596
+ // Validate that we got a valid number
597
+ if (isNaN(timeoutSeconds) || timeoutSeconds <= 0) {
598
+ timeoutSeconds = 300; // Default to 5 minutes
599
+ }
600
+
601
+ // Find or create the "No Response" category
602
+ let noResponseCategory = originalNode.router?.categories?.find(
603
+ (cat: any) => cat.name === 'No Response'
604
+ );
605
+
606
+ if (!noResponseCategory) {
607
+ noResponseCategory = {
608
+ uuid: generateUUID(),
609
+ name: 'No Response',
610
+ exit_uuid: generateUUID()
611
+ };
612
+
613
+ // Add to router categories
614
+ router.categories = router.categories || [];
615
+ router.categories.push(noResponseCategory);
616
+ }
617
+
618
+ waitConfig.timeout = {
619
+ seconds: timeoutSeconds,
620
+ category_uuid: noResponseCategory.uuid
621
+ };
622
+ }
623
+
624
+ router.wait = waitConfig;
625
+
626
+ return {
627
+ ...originalNode,
628
+ router
629
+ };
630
+ }
631
+
632
+ // Get existing router data for preservation
633
+ const existingCategories = originalNode.router?.categories || [];
634
+ const existingExits = originalNode.exits || [];
635
+ const existingCases = originalNode.router?.cases || [];
636
+
637
+ // Create router and exits using existing data when possible
638
+ const { router, exits } = createWaitForResponseRouter(
639
+ userRules,
640
+ existingCategories,
641
+ existingExits,
642
+ existingCases
643
+ );
644
+
645
+ // Build final router with wait configuration and result_name
646
+ const finalRouter: any = {
647
+ ...router,
26
648
  result_name: formData.result_name || 'response'
27
649
  };
28
650
 
651
+ // Build wait configuration based on form data
652
+ const waitConfig: any = {
653
+ type: 'msg'
654
+ };
655
+
656
+ try {
657
+ // Handle timeout configuration
658
+ if (formData.timeout_enabled) {
659
+ // Extract timeout value (handle both string and object formats)
660
+ let timeoutSeconds;
661
+
662
+ if (formData.timeout_duration) {
663
+ try {
664
+ timeoutSeconds = parseInt(formData.timeout_duration[0].value, 10);
665
+ } catch (e) {
666
+ timeoutSeconds = 300; // Default to 5 minutes
667
+ }
668
+ }
669
+
670
+ // Find or create the "No Response" category
671
+ const existingNoResponseCategory =
672
+ originalNode.router?.categories?.find(
673
+ (cat: any) => cat.name === 'No Response'
674
+ );
675
+
676
+ const noResponseCategory = existingNoResponseCategory || {
677
+ uuid: generateUUID(),
678
+ name: 'No Response',
679
+ exit_uuid: generateUUID()
680
+ };
681
+
682
+ waitConfig.timeout = {
683
+ seconds: timeoutSeconds,
684
+ category_uuid: noResponseCategory.uuid
685
+ };
686
+
687
+ // Ensure No Response category and exit exist
688
+ if (
689
+ !router.categories?.some((cat: any) => cat.name === 'No Response')
690
+ ) {
691
+ router.categories = router.categories || [];
692
+ router.categories.push(noResponseCategory);
693
+
694
+ // Add corresponding exit if it doesn't exist
695
+ if (
696
+ !exits.some(
697
+ (exit: any) => exit.uuid === noResponseCategory.exit_uuid
698
+ )
699
+ ) {
700
+ const noResponseExit = {
701
+ uuid: noResponseCategory.exit_uuid,
702
+ destination_uuid: existingNoResponseCategory?.exit_uuid
703
+ ? originalNode.exits?.find(
704
+ (exit) => exit.uuid === existingNoResponseCategory.exit_uuid
705
+ )?.destination_uuid || null
706
+ : null
707
+ };
708
+ exits.push(noResponseExit);
709
+ }
710
+ }
711
+ } else {
712
+ // Remove "No Response" category if timeout is disabled
713
+ if (router.categories) {
714
+ const noResponseCategoryIndex = router.categories.findIndex(
715
+ (cat: any) => cat.name === 'No Response'
716
+ );
717
+ if (noResponseCategoryIndex !== -1) {
718
+ const noResponseCategory =
719
+ router.categories[noResponseCategoryIndex];
720
+
721
+ // Remove the category
722
+ router.categories.splice(noResponseCategoryIndex, 1);
723
+
724
+ // Remove corresponding exit
725
+ const exitIndex = exits.findIndex(
726
+ (exit: any) => exit.uuid === noResponseCategory.exit_uuid
727
+ );
728
+ if (exitIndex !== -1) {
729
+ exits.splice(exitIndex, 1);
730
+ }
731
+ }
732
+ }
733
+ }
734
+ } catch (error) {
735
+ console.error('Error processing timeout configuration:', error);
736
+ // Continue without timeout in case of error
737
+ }
738
+
739
+ finalRouter.wait = waitConfig;
29
740
  return {
30
741
  ...originalNode,
31
- router
742
+ router: finalRouter,
743
+ exits: exits
32
744
  };
33
745
  }
34
746
  };