@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
@@ -7,7 +7,7 @@ interface Clip {
7
7
  height: number;
8
8
  }
9
9
 
10
- import { expect, fixture, html, assert, waitUntil } from '@open-wc/testing';
10
+ import { expect, fixture, html, assert } from '@open-wc/testing';
11
11
  import MouseHelper from './MouseHelper';
12
12
  import { Store } from '../src/store/Store';
13
13
  import { stub } from 'sinon';
@@ -200,18 +200,7 @@ export const waitForCondition = async (
200
200
  }
201
201
  };
202
202
 
203
- export const assertScreenshot = async (
204
- filename: string,
205
- clip: Clip,
206
- waitFor?: { clock?: any; predicate?: () => boolean }
207
- ) => {
208
- if (waitFor) {
209
- if (waitFor.clock) {
210
- waitFor.clock.restore();
211
- }
212
- await waitUntil(waitFor.predicate);
213
- }
214
-
203
+ export const assertScreenshot = async (filename: string, clip: Clip) => {
215
204
  // detect if we're running in copilot's environment and use adaptive threshold
216
205
  const isCopilotEnvironment = (window as any).isCopilotEnvironment;
217
206
  const threshold = isCopilotEnvironment ? 1.0 : 0.1;
@@ -217,6 +217,25 @@
217
217
  },
218
218
  "logs_url": null
219
219
  },
220
+ {
221
+ "type": "run_started",
222
+ "created_on": "2021-03-30T22:20:26.704467+00:00",
223
+ "run_uuid": "0198c846-2cf4-7992-9bd0-9b6581e42358",
224
+ "flow": {
225
+ "uuid": "d076d716-8071-417e-b188-d9746db223a1",
226
+ "name": "Favorites"
227
+ }
228
+ },
229
+ {
230
+ "type": "run_ended",
231
+ "created_on": "2021-03-30T22:20:26.704467+00:00",
232
+ "run_uuid": "0198c846-da2b-799f-8af9-aaf8857f947f",
233
+ "flow": {
234
+ "uuid": "d076d716-8071-417e-b188-d9746db223a1",
235
+ "name": "Favorites"
236
+ },
237
+ "status": "completed"
238
+ },
220
239
  {
221
240
  "uuid": "01988a77-979e-7768-a940-8d9c348e24fe",
222
241
  "type": "msg_received",
@@ -0,0 +1,18 @@
1
+ {
2
+ "next": null,
3
+ "previous": null,
4
+ "results": [
5
+ {
6
+ "uuid": "2399e7d6-fcdf-4e47-a835-f3bdb7f80938",
7
+ "name": "GPT 4.1",
8
+ "value": "2399e7d6-fcdf-4e47-a835-f3bdb7f80938",
9
+ "type": "openai"
10
+ },
11
+ {
12
+ "uuid": "4399e7d6-fcdf-4e47-a835-f3bdb7f80938",
13
+ "name": "GPT 5",
14
+ "value": "4399e7d6-fcdf-4e47-a835-f3bdb7f80938",
15
+ "type": "openai"
16
+ }
17
+ ]
18
+ }
@@ -60,6 +60,8 @@ html {
60
60
  --error-rgb: 255, 99, 71;
61
61
  --success-rgb: 102, 186, 104;
62
62
 
63
+ --color-label: #333;
64
+
63
65
  --selection-light-rgb: 240, 240, 240;
64
66
  --selection-dark-rgb: 180, 180, 180;
65
67
 
@@ -0,0 +1,523 @@
1
+ import { Client as MinioClient } from 'minio';
2
+ import busboy from 'busboy';
3
+ import { v4 as uuidv4 } from 'uuid';
4
+ import fs from 'fs';
5
+
6
+ /**
7
+ * Generates FlowInfo dynamically from a FlowDefinition
8
+ * This is a plain JavaScript version that can be imported by the web-dev-server
9
+ */
10
+ export function generateFlowInfo(definition) {
11
+ const dependencies = [];
12
+ const results = [];
13
+ const locals = [];
14
+
15
+ // Track unique dependencies by key/uuid to avoid duplicates
16
+ const dependencyMap = new Map();
17
+ // Track results by name to collect node_uuids
18
+ const resultMap = new Map();
19
+
20
+ // Process all nodes
21
+ definition.nodes.forEach((node) => {
22
+ // Process actions
23
+ node.actions.forEach((action) => {
24
+ extractDependenciesFromAction(action, dependencyMap, resultMap, node.uuid);
25
+ });
26
+
27
+ // Process router
28
+ if (node.router) {
29
+ extractDependenciesFromRouter(
30
+ node.router,
31
+ dependencyMap,
32
+ resultMap,
33
+ node.uuid
34
+ );
35
+ }
36
+ });
37
+
38
+ // Extract unique dependencies from map
39
+ dependencies.push(...Array.from(dependencyMap.values()));
40
+
41
+ // Extract results from map
42
+ results.push(...Array.from(resultMap.values()));
43
+
44
+ // Count languages from localization
45
+ const languageCount = Object.keys(definition.localization || {}).length;
46
+
47
+ return {
48
+ results,
49
+ dependencies,
50
+ counts: {
51
+ nodes: definition.nodes.length,
52
+ languages: languageCount
53
+ },
54
+ locals
55
+ };
56
+ }
57
+
58
+ function extractDependenciesFromAction(action, dependencyMap, resultMap, nodeUuid) {
59
+ switch (action.type) {
60
+ case 'set_contact_field':
61
+ if (action.field?.name) {
62
+ const key = `field:${action.field.name}`;
63
+ dependencyMap.set(key, {
64
+ uuid: action.field.uuid || '',
65
+ name: action.field.name,
66
+ type: 'field'
67
+ });
68
+ }
69
+ break;
70
+
71
+ case 'call_webhook':
72
+ if (action.url) {
73
+ const key = `webhook:${action.url}`;
74
+ dependencyMap.set(key, {
75
+ uuid: action.uuid,
76
+ name: action.url,
77
+ type: 'webhook'
78
+ });
79
+ }
80
+ break;
81
+
82
+ case 'add_contact_groups':
83
+ action.groups?.forEach((group) => {
84
+ if (group.name) {
85
+ const key = `group:${group.uuid || group.name}`;
86
+ dependencyMap.set(key, {
87
+ uuid: group.uuid || '',
88
+ name: group.name,
89
+ type: 'group'
90
+ });
91
+ }
92
+ });
93
+ break;
94
+
95
+ case 'remove_contact_groups':
96
+ action.groups?.forEach((group) => {
97
+ if (group.name) {
98
+ const key = `group:${group.uuid || group.name}`;
99
+ dependencyMap.set(key, {
100
+ uuid: group.uuid || '',
101
+ name: group.name,
102
+ type: 'group'
103
+ });
104
+ }
105
+ });
106
+ break;
107
+
108
+ case 'set_contact_channel':
109
+ if (action.channel?.name) {
110
+ const key = `channel:${action.channel.uuid}`;
111
+ dependencyMap.set(key, {
112
+ uuid: action.channel.uuid,
113
+ name: action.channel.name,
114
+ type: 'channel'
115
+ });
116
+ }
117
+ break;
118
+
119
+ case 'send_broadcast':
120
+ action.groups?.forEach((group) => {
121
+ if (group.name) {
122
+ const key = `group:${group.uuid || group.name}`;
123
+ dependencyMap.set(key, {
124
+ uuid: group.uuid || '',
125
+ name: group.name,
126
+ type: 'group'
127
+ });
128
+ }
129
+ });
130
+ action.contacts?.forEach((contact) => {
131
+ if (contact.name) {
132
+ const key = `contact:${contact.uuid}`;
133
+ dependencyMap.set(key, {
134
+ uuid: contact.uuid,
135
+ name: contact.name,
136
+ type: 'contact'
137
+ });
138
+ }
139
+ });
140
+ break;
141
+
142
+ case 'enter_flow':
143
+ if (action.flow?.name) {
144
+ const key = `flow:${action.flow.uuid}`;
145
+ dependencyMap.set(key, {
146
+ uuid: action.flow.uuid,
147
+ name: action.flow.name,
148
+ type: 'flow'
149
+ });
150
+ }
151
+ break;
152
+
153
+ case 'start_session':
154
+ if (action.flow?.name) {
155
+ const key = `flow:${action.flow.uuid}`;
156
+ dependencyMap.set(key, {
157
+ uuid: action.flow.uuid,
158
+ name: action.flow.name,
159
+ type: 'flow'
160
+ });
161
+ }
162
+ action.groups?.forEach((group) => {
163
+ if (group.name) {
164
+ const key = `group:${group.uuid || group.name}`;
165
+ dependencyMap.set(key, {
166
+ uuid: group.uuid || '',
167
+ name: group.name,
168
+ type: 'group'
169
+ });
170
+ }
171
+ });
172
+ break;
173
+
174
+ case 'call_classifier':
175
+ if (action.classifier?.name) {
176
+ const key = `classifier:${action.classifier.uuid}`;
177
+ dependencyMap.set(key, {
178
+ uuid: action.classifier.uuid,
179
+ name: action.classifier.name,
180
+ type: 'classifier'
181
+ });
182
+ }
183
+ break;
184
+
185
+ case 'call_resthook':
186
+ if (action.resthook) {
187
+ const key = `resthook:${action.resthook}`;
188
+ dependencyMap.set(key, {
189
+ uuid: action.uuid,
190
+ name: action.resthook,
191
+ type: 'resthook'
192
+ });
193
+ }
194
+ break;
195
+
196
+ case 'call_llm':
197
+ if (action.llm?.name) {
198
+ const key = `llm:${action.llm.uuid}`;
199
+ dependencyMap.set(key, {
200
+ uuid: action.llm.uuid,
201
+ name: action.llm.name,
202
+ type: 'llm'
203
+ });
204
+ }
205
+ break;
206
+
207
+ case 'open_ticket':
208
+ if (action.assignee?.name) {
209
+ const key = `user:${action.assignee.uuid}`;
210
+ dependencyMap.set(key, {
211
+ uuid: action.assignee.uuid,
212
+ name: action.assignee.name,
213
+ type: 'user'
214
+ });
215
+ }
216
+ if (action.topic?.name) {
217
+ const key = `topic:${action.topic.uuid}`;
218
+ dependencyMap.set(key, {
219
+ uuid: action.topic.uuid,
220
+ name: action.topic.name,
221
+ type: 'topic'
222
+ });
223
+ }
224
+ break;
225
+
226
+ case 'request_optin':
227
+ if (action.optin?.name) {
228
+ const key = `optin:${action.optin.uuid}`;
229
+ dependencyMap.set(key, {
230
+ uuid: action.optin.uuid,
231
+ name: action.optin.name,
232
+ type: 'optin'
233
+ });
234
+ }
235
+ break;
236
+
237
+ case 'add_input_labels':
238
+ action.labels?.forEach((label) => {
239
+ if (label.name) {
240
+ const key = `label:${label.uuid}`;
241
+ dependencyMap.set(key, {
242
+ uuid: label.uuid,
243
+ name: label.name,
244
+ type: 'label'
245
+ });
246
+ }
247
+ });
248
+ break;
249
+
250
+ case 'send_msg':
251
+ if (action.template?.name) {
252
+ const key = `template:${action.template.uuid}`;
253
+ dependencyMap.set(key, {
254
+ uuid: action.template.uuid,
255
+ name: action.template.name,
256
+ type: 'template'
257
+ });
258
+ }
259
+ break;
260
+
261
+ case 'set_run_result':
262
+ if (action.name) {
263
+ const existingResult = resultMap.get(action.name);
264
+ if (existingResult) {
265
+ // Add this node to existing result if not already present
266
+ if (!existingResult.node_uuids.includes(nodeUuid)) {
267
+ existingResult.node_uuids.push(nodeUuid);
268
+ }
269
+ // Add category if specified and not already present
270
+ if (action.category && !existingResult.categories.includes(action.category)) {
271
+ existingResult.categories.push(action.category);
272
+ }
273
+ } else {
274
+ // Create new result
275
+ const categories = action.category ? [action.category] : ['All Responses'];
276
+ resultMap.set(action.name, {
277
+ key: action.name.toLowerCase().replace(/[^a-z0-9_]/g, '_'),
278
+ name: action.name,
279
+ categories: categories,
280
+ node_uuids: [nodeUuid]
281
+ });
282
+ }
283
+ }
284
+ break;
285
+ }
286
+ }
287
+
288
+ function extractDependenciesFromRouter(router, dependencyMap, resultMap, nodeUuid) {
289
+ // Extract result information
290
+ if (router.result_name && router.categories) {
291
+ const existingResult = resultMap.get(router.result_name);
292
+ if (existingResult) {
293
+ // Add this node to existing result if not already present
294
+ if (!existingResult.node_uuids.includes(nodeUuid)) {
295
+ existingResult.node_uuids.push(nodeUuid);
296
+ }
297
+ } else {
298
+ // Create new result
299
+ const categories = router.categories.length > 0
300
+ ? router.categories.map((cat) => cat.name)
301
+ : ['All Responses'];
302
+
303
+ const result = {
304
+ key: router.result_name.toLowerCase().replace(/[^a-z0-9_]/g, '_'),
305
+ name: router.result_name,
306
+ categories: categories,
307
+ node_uuids: [nodeUuid]
308
+ };
309
+ resultMap.set(router.result_name, result);
310
+ }
311
+ }
312
+
313
+ // Process cases for potential dependencies
314
+ router.cases?.forEach((case_) => {
315
+ if (case_.type === 'has_group' && case_.arguments?.length >= 2) {
316
+ // Group dependency from split_by_groups
317
+ const groupUuid = case_.arguments[0];
318
+ const groupName = case_.arguments[1];
319
+ if (groupName) {
320
+ const key = `group:${groupUuid}`;
321
+ dependencyMap.set(key, {
322
+ uuid: groupUuid,
323
+ name: groupName,
324
+ type: 'group'
325
+ });
326
+ }
327
+ }
328
+ });
329
+ }
330
+
331
+ // Initialize Minio client for file uploads
332
+ export const minioClient = new MinioClient({
333
+ endPoint: 'minio',
334
+ port: 9000,
335
+ useSSL: false,
336
+ accessKey: 'root',
337
+ secretKey: 'tembatemba'
338
+ });
339
+
340
+ // Helper function to generate the correct public URL for uploaded files
341
+ export function getPublicUrl(bucketName, fileName, request) {
342
+ // Check if request is coming from localhost/127.0.0.1 (host machine)
343
+ // or from within docker network
344
+ const host = request.headers.host;
345
+ const userAgent = request.headers['user-agent'] || '';
346
+
347
+ // If accessing from host machine (localhost:3010), use localhost for minio too
348
+ if (host && host.startsWith('localhost:')) {
349
+ return `http://localhost:9000/${bucketName}/${fileName}`;
350
+ }
351
+
352
+ // If accessing from docker network, use internal hostname
353
+ return `http://minio:9000/${bucketName}/${fileName}`;
354
+ }
355
+
356
+ // Handle minio file uploads for media
357
+ export function handleMinioUpload(context) {
358
+ return new Promise((resolve) => {
359
+ try {
360
+ const bb = busboy({ headers: context.request.headers });
361
+ let fileInfo = null;
362
+ let fileBuffer = null;
363
+
364
+ bb.on('file', (name, file, info) => {
365
+ fileInfo = info;
366
+ const chunks = [];
367
+
368
+ file.on('data', (chunk) => {
369
+ chunks.push(chunk);
370
+ });
371
+
372
+ file.on('end', () => {
373
+ fileBuffer = Buffer.concat(chunks);
374
+ });
375
+ });
376
+
377
+ bb.on('finish', async () => {
378
+ if (!fileBuffer || !fileInfo) {
379
+ context.status = 400;
380
+ context.body = JSON.stringify({ error: 'No file uploaded' });
381
+ resolve();
382
+ return;
383
+ }
384
+
385
+ try {
386
+ const fileUuid = uuidv4();
387
+ const fileName = `${fileUuid}-${fileInfo.filename}`;
388
+ const bucketName = 'temba-attachments';
389
+
390
+ // Upload to minio
391
+ await minioClient.putObject(bucketName, fileName, fileBuffer, {
392
+ 'Content-Type': fileInfo.mimeType
393
+ });
394
+
395
+ // Return success response with appropriate URL based on request source
396
+ const publicUrl = getPublicUrl(bucketName, fileName, context.request);
397
+
398
+ // Debug logging
399
+ console.log('🔧 Upload Debug:', {
400
+ fileUuid,
401
+ fileName,
402
+ bucketName,
403
+ publicUrl,
404
+ contentType: fileInfo.mimeType,
405
+ host: context.request.headers.host
406
+ });
407
+
408
+ context.contentType = 'application/json';
409
+ context.body = JSON.stringify({
410
+ uuid: fileUuid,
411
+ content_type: fileInfo.mimeType,
412
+ url: publicUrl,
413
+ filename: fileInfo.filename,
414
+ size: fileBuffer.length
415
+ });
416
+
417
+ } catch (uploadError) {
418
+ console.error('Minio upload error:', uploadError);
419
+ context.status = 500;
420
+ context.body = JSON.stringify({
421
+ error: 'Upload failed',
422
+ details: uploadError.message
423
+ });
424
+ }
425
+
426
+ resolve();
427
+ });
428
+
429
+ context.req.pipe(bb);
430
+
431
+ } catch (error) {
432
+ console.error('File upload processing error:', error);
433
+ context.status = 500;
434
+ context.body = JSON.stringify({
435
+ error: 'Upload processing failed',
436
+ details: error.message
437
+ });
438
+ resolve();
439
+ }
440
+ });
441
+ }
442
+
443
+ // Handle label creation for the labels API
444
+ export function handleLabelCreation(context) {
445
+ return new Promise((resolve) => {
446
+ let body = '';
447
+
448
+ context.req.on('data', chunk => {
449
+ body += chunk.toString();
450
+ });
451
+
452
+ context.req.on('end', () => {
453
+ try {
454
+ const requestData = JSON.parse(body);
455
+ const labelName = requestData.name || '';
456
+
457
+ if (!labelName.trim()) {
458
+ context.status = 400;
459
+ context.body = JSON.stringify({ error: 'Label name is required' });
460
+ resolve();
461
+ return;
462
+ }
463
+
464
+ // Read existing labels file
465
+ const labelsPath = './static/api/labels.json';
466
+ let labelsData;
467
+
468
+ try {
469
+ labelsData = JSON.parse(fs.readFileSync(labelsPath, 'utf-8'));
470
+ } catch (error) {
471
+ // If file doesn't exist, create basic structure
472
+ labelsData = {
473
+ next: null,
474
+ previous: null,
475
+ results: []
476
+ };
477
+ }
478
+
479
+ // Check if label already exists
480
+ const existingLabel = labelsData.results.find(
481
+ label => label.name.toLowerCase() === labelName.trim().toLowerCase()
482
+ );
483
+
484
+ if (existingLabel) {
485
+ // Return existing label
486
+ context.contentType = 'application/json';
487
+ context.body = JSON.stringify(existingLabel);
488
+ resolve();
489
+ return;
490
+ }
491
+
492
+ // Create new label with UUID
493
+ const newLabel = {
494
+ uuid: uuidv4(),
495
+ name: labelName.trim(),
496
+ count: 0
497
+ };
498
+
499
+ // Add to labels data
500
+ labelsData.results.push(newLabel);
501
+
502
+ // Write back to file
503
+ fs.writeFileSync(labelsPath, JSON.stringify(labelsData, null, 2));
504
+
505
+ // Return the new label
506
+ context.contentType = 'application/json';
507
+ context.body = JSON.stringify(newLabel);
508
+
509
+ console.log('📝 Label created:', newLabel);
510
+
511
+ } catch (error) {
512
+ console.error('Label creation error:', error);
513
+ context.status = 500;
514
+ context.body = JSON.stringify({
515
+ error: 'Label creation failed',
516
+ details: error.message
517
+ });
518
+ }
519
+
520
+ resolve();
521
+ });
522
+ });
523
+ }