@nyaruka/temba-components 0.129.10 → 0.129.11

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 (243) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/demo/data/flows/sample-flow.json +96 -56
  3. package/dist/temba-components.js +278 -268
  4. package/dist/temba-components.js.map +1 -1
  5. package/out-tsc/src/events.js.map +1 -1
  6. package/out-tsc/src/flow/Editor.js +9 -6
  7. package/out-tsc/src/flow/Editor.js.map +1 -1
  8. package/out-tsc/src/flow/actions/set_contact_channel.js +1 -1
  9. package/out-tsc/src/flow/actions/set_contact_channel.js.map +1 -1
  10. package/out-tsc/src/flow/actions/set_contact_field.js +1 -1
  11. package/out-tsc/src/flow/actions/set_contact_field.js.map +1 -1
  12. package/out-tsc/src/flow/actions/set_contact_language.js +1 -1
  13. package/out-tsc/src/flow/actions/set_contact_language.js.map +1 -1
  14. package/out-tsc/src/flow/actions/set_contact_status.js +1 -1
  15. package/out-tsc/src/flow/actions/set_contact_status.js.map +1 -1
  16. package/out-tsc/src/flow/config.js +2 -8
  17. package/out-tsc/src/flow/config.js.map +1 -1
  18. package/out-tsc/src/flow/nodes/split_by_llm.js +101 -0
  19. package/out-tsc/src/flow/nodes/split_by_llm.js.map +1 -0
  20. package/out-tsc/src/flow/nodes/split_by_llm_categorize.js +4 -89
  21. package/out-tsc/src/flow/nodes/split_by_llm_categorize.js.map +1 -1
  22. package/out-tsc/src/flow/nodes/split_by_subflow.js +123 -3
  23. package/out-tsc/src/flow/nodes/split_by_subflow.js.map +1 -1
  24. package/out-tsc/src/flow/nodes/split_by_ticket.js +115 -13
  25. package/out-tsc/src/flow/nodes/split_by_ticket.js.map +1 -1
  26. package/out-tsc/src/flow/nodes/split_by_webhook.js +160 -12
  27. package/out-tsc/src/flow/nodes/split_by_webhook.js.map +1 -1
  28. package/out-tsc/src/live/ContactChat.js +48 -66
  29. package/out-tsc/src/live/ContactChat.js.map +1 -1
  30. package/out-tsc/src/utils.js +115 -0
  31. package/out-tsc/src/utils.js.map +1 -1
  32. package/out-tsc/test/nodes/split_by_llm.test.js +174 -0
  33. package/out-tsc/test/nodes/split_by_llm.test.js.map +1 -0
  34. package/package.json +1 -1
  35. package/screenshots/truth/actions/add_contact_groups/editor/descriptive-group-names.png +0 -0
  36. package/screenshots/truth/actions/add_contact_groups/editor/long-group-names.png +0 -0
  37. package/screenshots/truth/actions/add_contact_groups/editor/many-groups.png +0 -0
  38. package/screenshots/truth/actions/add_contact_groups/editor/multiple-groups.png +0 -0
  39. package/screenshots/truth/actions/add_contact_groups/editor/single-group.png +0 -0
  40. package/screenshots/truth/actions/call_llm/editor/information-extraction.png +0 -0
  41. package/screenshots/truth/actions/call_llm/editor/sentiment-analysis.png +0 -0
  42. package/screenshots/truth/actions/call_llm/editor/summarization.png +0 -0
  43. package/screenshots/truth/actions/call_llm/editor/translation-task.png +0 -0
  44. package/screenshots/truth/actions/remove_contact_groups/editor/cleanup-groups.png +0 -0
  45. package/screenshots/truth/actions/remove_contact_groups/editor/long-descriptive-group-names.png +0 -0
  46. package/screenshots/truth/actions/remove_contact_groups/editor/many-groups.png +0 -0
  47. package/screenshots/truth/actions/remove_contact_groups/editor/multiple-groups.png +0 -0
  48. package/screenshots/truth/actions/remove_contact_groups/editor/remove-from-all-groups.png +0 -0
  49. package/screenshots/truth/actions/remove_contact_groups/editor/single-group.png +0 -0
  50. package/screenshots/truth/actions/send_email/editor/complex-business-email.png +0 -0
  51. package/screenshots/truth/actions/send_email/editor/empty-body.png +0 -0
  52. package/screenshots/truth/actions/send_email/editor/empty-subject.png +0 -0
  53. package/screenshots/truth/actions/send_email/editor/long-subject.png +0 -0
  54. package/screenshots/truth/actions/send_email/editor/multiline-body.png +0 -0
  55. package/screenshots/truth/actions/send_email/editor/multiple-recipients.png +0 -0
  56. package/screenshots/truth/actions/send_email/editor/simple-email.png +0 -0
  57. package/screenshots/truth/actions/send_email/editor/with-expressions.png +0 -0
  58. package/screenshots/truth/actions/send_msg/editor/long-quick-replies.png +0 -0
  59. package/screenshots/truth/actions/send_msg/editor/multiline-text-with-replies.png +0 -0
  60. package/screenshots/truth/actions/send_msg/editor/simple-text.png +0 -0
  61. package/screenshots/truth/actions/send_msg/editor/text-with-linebreaks.png +0 -0
  62. package/screenshots/truth/actions/send_msg/editor/text-with-many-quick-replies.png +0 -0
  63. package/screenshots/truth/actions/send_msg/editor/text-with-quick-replies.png +0 -0
  64. package/screenshots/truth/actions/send_msg/editor/text-without-quick-replies.png +0 -0
  65. package/screenshots/truth/checkbox/checkbox-label-background-hover.png +0 -0
  66. package/screenshots/truth/checkbox/checkbox-no-label-no-background-hover.png +0 -0
  67. package/screenshots/truth/checkbox/checkbox-whitespace-label-no-background-hover.png +0 -0
  68. package/screenshots/truth/checkbox/checkbox-with-help-text.png +0 -0
  69. package/screenshots/truth/checkbox/checked.png +0 -0
  70. package/screenshots/truth/checkbox/default.png +0 -0
  71. package/screenshots/truth/colorpicker/default.png +0 -0
  72. package/screenshots/truth/colorpicker/focused.png +0 -0
  73. package/screenshots/truth/colorpicker/initialized.png +0 -0
  74. package/screenshots/truth/colorpicker/selected.png +0 -0
  75. package/screenshots/truth/compose/attachments-tab.png +0 -0
  76. package/screenshots/truth/compose/attachments-with-files.png +0 -0
  77. package/screenshots/truth/compose/intial-text.png +0 -0
  78. package/screenshots/truth/compose/no-counter.png +0 -0
  79. package/screenshots/truth/compose/wraps-text-and-spaces.png +0 -0
  80. package/screenshots/truth/compose/wraps-text-and-url.png +0 -0
  81. package/screenshots/truth/compose/wraps-text-no-spaces.png +0 -0
  82. package/screenshots/truth/contacts/chat-failure.png +0 -0
  83. package/screenshots/truth/contacts/chat-for-active-contact.png +0 -0
  84. package/screenshots/truth/contacts/chat-sends-attachments-only.png +0 -0
  85. package/screenshots/truth/contacts/chat-sends-text-and-attachments.png +0 -0
  86. package/screenshots/truth/contacts/chat-sends-text-only.png +0 -0
  87. package/screenshots/truth/counter/summary.png +0 -0
  88. package/screenshots/truth/counter/text.png +0 -0
  89. package/screenshots/truth/counter/unicode-variables.png +0 -0
  90. package/screenshots/truth/counter/unicode.png +0 -0
  91. package/screenshots/truth/counter/variable.png +0 -0
  92. package/screenshots/truth/datepicker/date-truncated-time.png +0 -0
  93. package/screenshots/truth/datepicker/date.png +0 -0
  94. package/screenshots/truth/datepicker/initial-timezone.png +0 -0
  95. package/screenshots/truth/datepicker/range-picker-editing-start.png +0 -0
  96. package/screenshots/truth/datepicker/updated-keyboard-date.png +0 -0
  97. package/screenshots/truth/dialog/focused.png +0 -0
  98. package/screenshots/truth/dropdown/right-edge-collision.png +0 -0
  99. package/screenshots/truth/editor/router.png +0 -0
  100. package/screenshots/truth/editor/send_msg.png +0 -0
  101. package/screenshots/truth/editor/set_contact_language.png +0 -0
  102. package/screenshots/truth/editor/set_contact_name.png +0 -0
  103. package/screenshots/truth/editor/set_run_result.png +0 -0
  104. package/screenshots/truth/editor/wait.png +0 -0
  105. package/screenshots/truth/field-renderer/checkbox-checked.png +0 -0
  106. package/screenshots/truth/field-renderer/checkbox-unchecked.png +0 -0
  107. package/screenshots/truth/field-renderer/checkbox-with-errors.png +0 -0
  108. package/screenshots/truth/field-renderer/context-comparison.png +0 -0
  109. package/screenshots/truth/field-renderer/key-value-with-label.png +0 -0
  110. package/screenshots/truth/field-renderer/message-editor-with-label.png +0 -0
  111. package/screenshots/truth/field-renderer/select-multi.png +0 -0
  112. package/screenshots/truth/field-renderer/select-no-label.png +0 -0
  113. package/screenshots/truth/field-renderer/select-with-label.png +0 -0
  114. package/screenshots/truth/field-renderer/text-evaluated.png +0 -0
  115. package/screenshots/truth/field-renderer/text-no-label.png +0 -0
  116. package/screenshots/truth/field-renderer/text-with-errors.png +0 -0
  117. package/screenshots/truth/field-renderer/text-with-label.png +0 -0
  118. package/screenshots/truth/field-renderer/textarea-evaluated.png +0 -0
  119. package/screenshots/truth/field-renderer/textarea-with-label.png +0 -0
  120. package/screenshots/truth/integration/checkbox-markdown-errors.png +0 -0
  121. package/screenshots/truth/list/fields-dragging.png +0 -0
  122. package/screenshots/truth/list/fields-filtered.png +0 -0
  123. package/screenshots/truth/list/fields-hovered.png +0 -0
  124. package/screenshots/truth/list/fields.png +0 -0
  125. package/screenshots/truth/list/items-selected.png +0 -0
  126. package/screenshots/truth/list/items-updated.png +0 -0
  127. package/screenshots/truth/list/items.png +0 -0
  128. package/screenshots/truth/menu/menu-focused-with items.png +0 -0
  129. package/screenshots/truth/menu/menu-refresh-1.png +0 -0
  130. package/screenshots/truth/menu/menu-refresh-2.png +0 -0
  131. package/screenshots/truth/menu/menu-submenu.png +0 -0
  132. package/screenshots/truth/menu/menu-tasks-nextup.png +0 -0
  133. package/screenshots/truth/menu/menu-tasks.png +0 -0
  134. package/screenshots/truth/message-editor/autogrow-initial-content.png +0 -0
  135. package/screenshots/truth/message-editor/default.png +0 -0
  136. package/screenshots/truth/message-editor/drag-highlight.png +0 -0
  137. package/screenshots/truth/message-editor/filtered-attachments.png +0 -0
  138. package/screenshots/truth/message-editor/with-completion.png +0 -0
  139. package/screenshots/truth/message-editor/with-properties.png +0 -0
  140. package/screenshots/truth/modax/form.png +0 -0
  141. package/screenshots/truth/modax/simple.png +0 -0
  142. package/screenshots/truth/nodes/split_by_llm/editor/information-extraction.png +0 -0
  143. package/screenshots/truth/nodes/split_by_llm/editor/sentiment-analysis.png +0 -0
  144. package/screenshots/truth/nodes/split_by_llm/editor/summarization.png +0 -0
  145. package/screenshots/truth/nodes/split_by_llm/editor/translation-task.png +0 -0
  146. package/screenshots/truth/nodes/split_by_llm/render/information-extraction.png +0 -0
  147. package/screenshots/truth/nodes/split_by_llm/render/sentiment-analysis.png +0 -0
  148. package/screenshots/truth/nodes/split_by_llm/render/summarization.png +0 -0
  149. package/screenshots/truth/nodes/split_by_llm/render/translation-task.png +0 -0
  150. package/screenshots/truth/nodes/split_by_llm_categorize/editor/basic-categorization.png +0 -0
  151. package/screenshots/truth/nodes/split_by_llm_categorize/editor/custom-input-and-result-name.png +0 -0
  152. package/screenshots/truth/nodes/split_by_llm_categorize/editor/feedback-categorization.png +0 -0
  153. package/screenshots/truth/nodes/split_by_llm_categorize/editor/many-categories.png +0 -0
  154. package/screenshots/truth/nodes/split_by_llm_categorize/editor/minimal-categories.png +0 -0
  155. package/screenshots/truth/nodes/split_by_random/editor/ab-test-multiple-variants.png +0 -0
  156. package/screenshots/truth/nodes/split_by_random/editor/sampling-split.png +0 -0
  157. package/screenshots/truth/nodes/split_by_random/editor/three-way-split.png +0 -0
  158. package/screenshots/truth/nodes/split_by_random/editor/two-bucket-split.png +0 -0
  159. package/screenshots/truth/nodes/wait_for_digits/editor/basic-digits-wait.png +0 -0
  160. package/screenshots/truth/nodes/wait_for_digits/editor/phone-number-collection.png +0 -0
  161. package/screenshots/truth/nodes/wait_for_digits/editor/single-digit-with-timeout.png +0 -0
  162. package/screenshots/truth/nodes/wait_for_digits/editor/verification-code.png +0 -0
  163. package/screenshots/truth/nodes/wait_for_response/editor/basic-wait.png +0 -0
  164. package/screenshots/truth/nodes/wait_for_response/editor/custom-result-name.png +0 -0
  165. package/screenshots/truth/nodes/wait_for_response/editor/no-timeout.png +0 -0
  166. package/screenshots/truth/nodes/wait_for_response/editor/short-timeout.png +0 -0
  167. package/screenshots/truth/omnibox/selected.png +0 -0
  168. package/screenshots/truth/options/block.png +0 -0
  169. package/screenshots/truth/run-list/basic.png +0 -0
  170. package/screenshots/truth/select/disabled-multi-selection.png +0 -0
  171. package/screenshots/truth/select/disabled-selection.png +0 -0
  172. package/screenshots/truth/select/disabled.png +0 -0
  173. package/screenshots/truth/select/embedded.png +0 -0
  174. package/screenshots/truth/select/empty-options.png +0 -0
  175. package/screenshots/truth/select/expression-selected.png +0 -0
  176. package/screenshots/truth/select/expressions.png +0 -0
  177. package/screenshots/truth/select/functions.png +0 -0
  178. package/screenshots/truth/select/local-options.png +0 -0
  179. package/screenshots/truth/select/multi-with-endpoint.png +0 -0
  180. package/screenshots/truth/select/multiple-initial-values.png +0 -0
  181. package/screenshots/truth/select/remote-options.png +0 -0
  182. package/screenshots/truth/select/search-enabled.png +0 -0
  183. package/screenshots/truth/select/search-multi-no-matches.png +0 -0
  184. package/screenshots/truth/select/search-selected-focus.png +0 -0
  185. package/screenshots/truth/select/search-selected.png +0 -0
  186. package/screenshots/truth/select/search-with-selected.png +0 -0
  187. package/screenshots/truth/select/searching.png +0 -0
  188. package/screenshots/truth/select/selected-multi-maxitems-reached.png +0 -0
  189. package/screenshots/truth/select/selected-multi.png +0 -0
  190. package/screenshots/truth/select/selected-single.png +0 -0
  191. package/screenshots/truth/select/selection-clearable.png +0 -0
  192. package/screenshots/truth/select/static-initial-value.png +0 -0
  193. package/screenshots/truth/select/static-initial-via-selected.png +0 -0
  194. package/screenshots/truth/select/truncated-selection.png +0 -0
  195. package/screenshots/truth/select/with-placeholder.png +0 -0
  196. package/screenshots/truth/select/without-placeholder.png +0 -0
  197. package/screenshots/truth/slider/update-slider-on-circle-dragged.png +0 -0
  198. package/screenshots/truth/templates/default.png +0 -0
  199. package/screenshots/truth/templates/unapproved.png +0 -0
  200. package/screenshots/truth/textinput/autogrow-initial.png +0 -0
  201. package/screenshots/truth/textinput/input-disabled.png +0 -0
  202. package/screenshots/truth/textinput/input-focused.png +0 -0
  203. package/screenshots/truth/textinput/input-form.png +0 -0
  204. package/screenshots/truth/textinput/input-inserted.png +0 -0
  205. package/screenshots/truth/textinput/input-placeholder.png +0 -0
  206. package/screenshots/truth/textinput/input-updated.png +0 -0
  207. package/screenshots/truth/textinput/input.png +0 -0
  208. package/screenshots/truth/textinput/textarea-focused.png +0 -0
  209. package/screenshots/truth/textinput/textarea.png +0 -0
  210. package/src/events.ts +4 -2
  211. package/src/flow/Editor.ts +6 -3
  212. package/src/flow/actions/set_contact_channel.ts +1 -1
  213. package/src/flow/actions/set_contact_field.ts +1 -1
  214. package/src/flow/actions/set_contact_language.ts +1 -1
  215. package/src/flow/actions/set_contact_status.ts +1 -1
  216. package/src/flow/config.ts +2 -8
  217. package/src/flow/nodes/split_by_llm.ts +119 -0
  218. package/src/flow/nodes/split_by_llm_categorize.ts +13 -116
  219. package/src/flow/nodes/split_by_subflow.ts +153 -3
  220. package/src/flow/nodes/split_by_ticket.ts +135 -12
  221. package/src/flow/nodes/split_by_webhook.ts +187 -12
  222. package/src/live/ContactChat.ts +56 -58
  223. package/src/store/flow-definition.d.ts +2 -1
  224. package/src/utils.ts +192 -0
  225. package/test/nodes/split_by_llm.test.ts +232 -0
  226. package/test-assets/style.css +36 -234
  227. package/web-dev-server.config.mjs +26 -0
  228. package/web-test-runner.config.mjs +1 -1
  229. package/out-tsc/src/flow/actions/call_llm.js +0 -64
  230. package/out-tsc/src/flow/actions/call_llm.js.map +0 -1
  231. package/out-tsc/src/flow/actions/call_webhook.js +0 -131
  232. package/out-tsc/src/flow/actions/call_webhook.js.map +0 -1
  233. package/out-tsc/src/flow/actions/enter_flow.js +0 -14
  234. package/out-tsc/src/flow/actions/enter_flow.js.map +0 -1
  235. package/out-tsc/src/flow/actions/open_ticket.js +0 -73
  236. package/out-tsc/src/flow/actions/open_ticket.js.map +0 -1
  237. package/out-tsc/test/actions/call_llm.test.js +0 -103
  238. package/out-tsc/test/actions/call_llm.test.js.map +0 -1
  239. package/src/flow/actions/call_llm.ts +0 -66
  240. package/src/flow/actions/call_webhook.ts +0 -143
  241. package/src/flow/actions/enter_flow.ts +0 -15
  242. package/src/flow/actions/open_ticket.ts +0 -83
  243. package/test/actions/call_llm.test.ts +0 -137
@@ -27,6 +27,7 @@ import {
27
27
  ContactGroupsEvent,
28
28
  ContactHistoryPage,
29
29
  ContactLanguageChangedEvent,
30
+ ContactStatusChangedEvent,
30
31
  MsgEvent,
31
32
  NameChangedEvent,
32
33
  OptInEvent,
@@ -61,6 +62,7 @@ export enum Events {
61
62
  CONTACT_GROUPS_CHANGED = 'contact_groups_changed',
62
63
  CONTACT_LANGUAGE_CHANGED = 'contact_language_changed',
63
64
  CONTACT_NAME_CHANGED = 'contact_name_changed',
65
+ CONTACT_STATUS_CHANGED = 'contact_status_changed',
64
66
  CONTACT_URNS_CHANGED = 'contact_urns_changed',
65
67
  IVR_CREATED = 'ivr_created',
66
68
  MSG_CREATED = 'msg_created',
@@ -97,28 +99,10 @@ const renderInfoList = (singular: string, plural: string, items: any[]) => {
97
99
  };
98
100
 
99
101
  const renderChannelEvent = (event: ChannelEvent): string => {
100
- if (event.event.type === 'mt_miss') {
101
- return 'Missed outgoing call';
102
- } else if (event.event.type === 'mo_miss') {
103
- return 'Missed incoming call';
104
- } else if (event.event.type === 'new_conversation') {
105
- return 'Started conversation';
106
- } else if (event.channel_event_type === 'welcome_message') {
107
- return 'Welcome Message Sent';
108
- } else if (event.event.type === 'referral') {
109
- return 'Referred';
110
- } else if (event.event.type === 'follow') {
111
- return 'Followed';
102
+ if (event.channel_event_type === 'welcome_message') {
103
+ return 'Welcome message sent';
112
104
  } else if (event.event.type === 'stop_contact') {
113
105
  return 'Stopped';
114
- } else if (event.event.type === 'mt_call') {
115
- return 'Outgoing Phone Call'; // deprecated
116
- } else if (event.event.type == 'mo_call') {
117
- return 'Incoming Phone call'; // deprecated
118
- } else if (event.event.type == 'optin') {
119
- return `Opted in to **${event.event.optin?.name}**`; // deprecated
120
- } else if (event.event.type == 'optout') {
121
- return `Opted out of **${event.event.optin?.name}**`; // deprecated
122
106
  }
123
107
  };
124
108
 
@@ -220,6 +204,12 @@ export const renderContactLanguageChangedEvent = (
220
204
  return `Language updated to **${event.language}**`;
221
205
  };
222
206
 
207
+ export const renderContactStatusChangedEvent = (
208
+ event: ContactStatusChangedEvent
209
+ ): string => {
210
+ return `Status updated to **${event.status}**`;
211
+ };
212
+
223
213
  export const renderCallEvent = (event: CallEvent): string => {
224
214
  if (event.type === Events.CALL_CREATED) {
225
215
  return `Call started`;
@@ -232,7 +222,7 @@ export const renderCallEvent = (event: CallEvent): string => {
232
222
 
233
223
  export const renderOptInEvent = (event: OptInEvent): string => {
234
224
  if (event.type === Events.OPTIN_REQUESTED) {
235
- return `Requested opt-in for ${event.optin.name}`;
225
+ return `Requested opt-in for **${event.optin.name}**`;
236
226
  } else if (event.type === Events.OPTIN_STARTED) {
237
227
  return `Opted in to **${event.optin.name}**`;
238
228
  } else if (event.type === Events.OPTIN_STOPPED) {
@@ -632,53 +622,58 @@ export class ContactChat extends ContactStoreElement {
632
622
  public getEventMessage(event: ContactEvent): ChatEvent {
633
623
  let message = null;
634
624
  switch (event.type) {
635
- case Events.TICKET_OPENED:
625
+ case Events.AIRTIME_TRANSFERRED:
636
626
  message = {
637
627
  type: MessageType.Inline,
638
- text: renderTicketAction(event as TicketEvent, 'opened')
628
+ text: renderAirtimeTransferredEvent(event as AirtimeTransferredEvent)
639
629
  };
640
630
  break;
641
- case Events.TICKET_ASSIGNED:
631
+ case Events.CALL_CREATED:
632
+ case Events.CALL_MISSED:
633
+ case Events.CALL_RECEIVED:
642
634
  message = {
643
635
  type: MessageType.Inline,
644
- text: renderTicketAssigned(event as TicketEvent)
636
+ text: renderCallEvent(event as CallEvent)
645
637
  };
646
638
  break;
647
- case Events.TICKET_REOPENED:
639
+ case Events.CHAT_STARTED:
648
640
  message = {
649
641
  type: MessageType.Inline,
650
- text: renderTicketAction(event as TicketEvent, 'reopened')
642
+ text: renderChatStartedEvent(event as ChatStartedEvent)
651
643
  };
652
644
  break;
653
- case Events.TICKET_CLOSED:
645
+ case Events.CONTACT_FIELD_CHANGED:
654
646
  message = {
655
647
  type: MessageType.Inline,
656
- text: renderTicketAction(event as TicketEvent, 'closed')
648
+ text: renderUpdateEvent(event as UpdateFieldEvent)
657
649
  };
658
650
  break;
659
- case Events.TICKET_TOPIC_CHANGED:
651
+ case Events.CONTACT_GROUPS_CHANGED:
660
652
  message = {
661
653
  type: MessageType.Inline,
662
- text: `Topic changed to **${(event as TicketEvent).topic.name}**`
654
+ text: renderContactGroupsEvent(event as ContactGroupsEvent)
663
655
  };
664
656
  break;
665
- case Events.RUN_STARTED:
666
- case Events.RUN_ENDED:
657
+ case Events.CONTACT_LANGUAGE_CHANGED:
667
658
  message = {
668
659
  type: MessageType.Inline,
669
- text: renderRunEvent(event as RunEvent)
660
+ text: renderContactLanguageChangedEvent(
661
+ event as ContactLanguageChangedEvent
662
+ )
670
663
  };
671
664
  break;
672
- case Events.CONTACT_FIELD_CHANGED:
665
+ case Events.CONTACT_NAME_CHANGED:
673
666
  message = {
674
667
  type: MessageType.Inline,
675
- text: renderUpdateEvent(event as UpdateFieldEvent)
668
+ text: renderNameChanged(event as NameChangedEvent)
676
669
  };
677
670
  break;
678
- case Events.CONTACT_NAME_CHANGED:
671
+ case Events.CONTACT_STATUS_CHANGED:
679
672
  message = {
680
673
  type: MessageType.Inline,
681
- text: renderNameChanged(event as NameChangedEvent)
674
+ text: renderContactStatusChangedEvent(
675
+ event as ContactStatusChangedEvent
676
+ )
682
677
  };
683
678
  break;
684
679
  case Events.CONTACT_URNS_CHANGED:
@@ -687,52 +682,55 @@ export class ContactChat extends ContactStoreElement {
687
682
  text: renderContactURNsChanged(event as URNsChangedEvent)
688
683
  };
689
684
  break;
690
- case Events.CONTACT_GROUPS_CHANGED:
685
+ case Events.OPTIN_REQUESTED:
686
+ case Events.OPTIN_STARTED:
687
+ case Events.OPTIN_STOPPED:
691
688
  message = {
692
689
  type: MessageType.Inline,
693
- text: renderContactGroupsEvent(event as ContactGroupsEvent)
690
+ text: renderOptInEvent(event as OptInEvent)
694
691
  };
695
692
  break;
696
- case Events.AIRTIME_TRANSFERRED:
693
+ case Events.RUN_STARTED:
694
+ case Events.RUN_ENDED:
697
695
  message = {
698
696
  type: MessageType.Inline,
699
- text: renderAirtimeTransferredEvent(event as AirtimeTransferredEvent)
697
+ text: renderRunEvent(event as RunEvent)
700
698
  };
701
699
  break;
702
- case Events.CALL_CREATED:
703
- case Events.CALL_MISSED:
704
- case Events.CALL_RECEIVED:
700
+ case Events.TICKET_ASSIGNED:
705
701
  message = {
706
702
  type: MessageType.Inline,
707
- text: renderCallEvent(event as CallEvent)
703
+ text: renderTicketAssigned(event as TicketEvent)
708
704
  };
709
705
  break;
710
- case Events.CHANNEL_EVENT:
706
+ case Events.TICKET_CLOSED:
711
707
  message = {
712
708
  type: MessageType.Inline,
713
- text: renderChannelEvent(event as ChannelEvent)
709
+ text: renderTicketAction(event as TicketEvent, 'closed')
714
710
  };
715
711
  break;
716
- case Events.CHAT_STARTED:
712
+ case Events.TICKET_OPENED:
717
713
  message = {
718
714
  type: MessageType.Inline,
719
- text: renderChatStartedEvent(event as ChatStartedEvent)
715
+ text: renderTicketAction(event as TicketEvent, 'opened')
720
716
  };
721
717
  break;
722
- case Events.CONTACT_LANGUAGE_CHANGED:
718
+ case Events.TICKET_REOPENED:
723
719
  message = {
724
720
  type: MessageType.Inline,
725
- text: renderContactLanguageChangedEvent(
726
- event as ContactLanguageChangedEvent
727
- )
721
+ text: renderTicketAction(event as TicketEvent, 'reopened')
728
722
  };
729
723
  break;
730
- case Events.OPTIN_REQUESTED:
731
- case Events.OPTIN_STARTED:
732
- case Events.OPTIN_STOPPED:
724
+ case Events.TICKET_TOPIC_CHANGED:
733
725
  message = {
734
726
  type: MessageType.Inline,
735
- text: renderOptInEvent(event as OptInEvent)
727
+ text: `Topic changed to **${(event as TicketEvent).topic.name}**`
728
+ };
729
+ break;
730
+ case Events.CHANNEL_EVENT: // deprecated
731
+ message = {
732
+ type: MessageType.Inline,
733
+ text: renderChannelEvent(event as ChannelEvent)
736
734
  };
737
735
  break;
738
736
  }
@@ -175,7 +175,8 @@ export interface CallLLM extends Action {
175
175
  llm: NamedObject;
176
176
  instructions: string;
177
177
  input: string;
178
- result_name: string;
178
+ result_name?: string;
179
+ output_local?: string;
179
180
  }
180
181
 
181
182
  export interface OpenTicket extends Action {
package/src/utils.ts CHANGED
@@ -932,3 +932,195 @@ export const getMiddle = (a: DOMRect, b: DOMRect) => {
932
932
 
933
933
  // Export the UUID function from the uuid package
934
934
  export { generateUUID };
935
+
936
+ // Helper types for router creation
937
+ export interface RouterCategory {
938
+ uuid: string;
939
+ name: string;
940
+ exit_uuid: string;
941
+ }
942
+
943
+ export interface RouterExit {
944
+ uuid: string;
945
+ destination_uuid: string | null;
946
+ }
947
+
948
+ export interface RouterCase {
949
+ uuid: string;
950
+ type: string;
951
+ arguments: string[];
952
+ category_uuid: string;
953
+ }
954
+
955
+ export interface CreateCategoryParams {
956
+ name: string;
957
+ existingCategories: any[];
958
+ existingExits: any[];
959
+ existingCases?: any[];
960
+ caseConfig?: {
961
+ type: string;
962
+ arguments: string[];
963
+ };
964
+ }
965
+
966
+ // Helper function to create or preserve a category with its exit and case
967
+ export const createOrPreserveCategory = (
968
+ params: CreateCategoryParams
969
+ ): {
970
+ category: RouterCategory;
971
+ exit: RouterExit;
972
+ case?: RouterCase;
973
+ } => {
974
+ const { name, existingCategories, existingExits, existingCases, caseConfig } =
975
+ params;
976
+
977
+ // Find existing category
978
+ const existingCategory = existingCategories.find((cat) => cat.name === name);
979
+ const existingExit = existingCategory
980
+ ? existingExits.find((exit) => exit.uuid === existingCategory.exit_uuid)
981
+ : null;
982
+ const existingCase =
983
+ existingCategory && existingCases
984
+ ? existingCases.find(
985
+ (case_) => case_.category_uuid === existingCategory.uuid
986
+ )
987
+ : null;
988
+
989
+ // Use existing UUIDs or generate new ones
990
+ const categoryUuid = existingCategory?.uuid || generateUUID();
991
+ const exitUuid = existingExit?.uuid || generateUUID();
992
+ const caseUuid = existingCase?.uuid || generateUUID();
993
+
994
+ const category: RouterCategory = {
995
+ uuid: categoryUuid,
996
+ name: name,
997
+ exit_uuid: exitUuid
998
+ };
999
+
1000
+ const exit: RouterExit = {
1001
+ uuid: exitUuid,
1002
+ destination_uuid: existingExit?.destination_uuid || null
1003
+ };
1004
+
1005
+ const result: {
1006
+ category: RouterCategory;
1007
+ exit: RouterExit;
1008
+ case?: RouterCase;
1009
+ } = { category, exit };
1010
+
1011
+ if (caseConfig) {
1012
+ result.case = {
1013
+ uuid: caseUuid,
1014
+ type: caseConfig.type,
1015
+ arguments: caseConfig.arguments,
1016
+ category_uuid: categoryUuid
1017
+ };
1018
+ }
1019
+
1020
+ return result;
1021
+ };
1022
+
1023
+ // Helper function to create a complete router with standard Success/Failure categories
1024
+ export const createSuccessFailureRouter = (
1025
+ operand: string,
1026
+ successCaseConfig: { type: string; arguments: string[] },
1027
+ existingCategories: any[] = [],
1028
+ existingExits: any[] = [],
1029
+ existingCases: any[] = []
1030
+ ) => {
1031
+ const success = createOrPreserveCategory({
1032
+ name: 'Success',
1033
+ existingCategories,
1034
+ existingExits,
1035
+ existingCases,
1036
+ caseConfig: successCaseConfig
1037
+ });
1038
+
1039
+ const failure = createOrPreserveCategory({
1040
+ name: 'Failure',
1041
+ existingCategories,
1042
+ existingExits
1043
+ });
1044
+
1045
+ return {
1046
+ router: {
1047
+ type: 'switch' as const,
1048
+ categories: [success.category, failure.category],
1049
+ default_category_uuid: failure.category.uuid,
1050
+ operand: operand,
1051
+ cases: success.case ? [success.case] : []
1052
+ },
1053
+ exits: [success.exit, failure.exit]
1054
+ };
1055
+ };
1056
+
1057
+ // Helper function to create a multi-category router with Other/Failure defaults
1058
+ export const createMultiCategoryRouter = (
1059
+ operand: string,
1060
+ userCategories: string[],
1061
+ categoryToCaseMap: (categoryName: string) => {
1062
+ type: string;
1063
+ arguments: string[];
1064
+ },
1065
+ existingCategories: any[] = [],
1066
+ existingExits: any[] = [],
1067
+ existingCases: any[] = []
1068
+ ) => {
1069
+ const categories: RouterCategory[] = [];
1070
+ const exits: RouterExit[] = [];
1071
+ const cases: RouterCase[] = [];
1072
+
1073
+ // Add user categories
1074
+ userCategories.forEach((categoryName) => {
1075
+ const result = createOrPreserveCategory({
1076
+ name: categoryName,
1077
+ existingCategories,
1078
+ existingExits,
1079
+ existingCases,
1080
+ caseConfig: categoryToCaseMap(categoryName)
1081
+ });
1082
+
1083
+ categories.push(result.category);
1084
+ exits.push(result.exit);
1085
+ if (result.case) {
1086
+ cases.push(result.case);
1087
+ }
1088
+ });
1089
+
1090
+ // Add "Other" category (default)
1091
+ const other = createOrPreserveCategory({
1092
+ name: 'Other',
1093
+ existingCategories,
1094
+ existingExits
1095
+ });
1096
+ categories.push(other.category);
1097
+ exits.push(other.exit);
1098
+
1099
+ // Add "Failure" category
1100
+ const failure = createOrPreserveCategory({
1101
+ name: 'Failure',
1102
+ existingCategories,
1103
+ existingExits,
1104
+ existingCases,
1105
+ caseConfig: {
1106
+ type: 'has_only_text',
1107
+ arguments: ['<ERROR>']
1108
+ }
1109
+ });
1110
+ categories.push(failure.category);
1111
+ exits.push(failure.exit);
1112
+ if (failure.case) {
1113
+ cases.push(failure.case);
1114
+ }
1115
+
1116
+ return {
1117
+ router: {
1118
+ type: 'switch' as const,
1119
+ categories: categories,
1120
+ default_category_uuid: other.category.uuid,
1121
+ operand: operand,
1122
+ cases: cases
1123
+ },
1124
+ exits: exits
1125
+ };
1126
+ };
@@ -0,0 +1,232 @@
1
+ import { expect } from '@open-wc/testing';
2
+ import { split_by_llm } from '../../src/flow/nodes/split_by_llm';
3
+ import { Node, CallLLM } from '../../src/store/flow-definition';
4
+ import { NodeTest } from '../NodeHelper';
5
+
6
+ /**
7
+ * Test suite for the split_by_llm node configuration.
8
+ */
9
+ describe('split_by_llm node config', () => {
10
+ const helper = new NodeTest(split_by_llm, 'split_by_llm');
11
+
12
+ describe('basic properties', () => {
13
+ helper.testBasicProperties();
14
+
15
+ it('has correct name', () => {
16
+ expect(split_by_llm.name).to.equal('Call AI');
17
+ });
18
+
19
+ it('has form configuration', () => {
20
+ expect(split_by_llm.form).to.exist;
21
+ expect(split_by_llm.form.llm).to.exist;
22
+ expect(split_by_llm.form.instructions).to.exist;
23
+ expect(split_by_llm.form.input).to.exist;
24
+ });
25
+
26
+ it('has layout configuration', () => {
27
+ expect(split_by_llm.layout).to.exist;
28
+ expect(split_by_llm.layout).to.deep.equal([
29
+ 'llm',
30
+ 'input',
31
+ 'instructions'
32
+ ]);
33
+ });
34
+ });
35
+
36
+ describe('toFormData', () => {
37
+ it('extracts data from node with call_llm action', () => {
38
+ const node: Node = {
39
+ uuid: 'test-node',
40
+ actions: [
41
+ {
42
+ uuid: 'test-action',
43
+ type: 'call_llm',
44
+ llm: { uuid: 'gpt-4', name: 'GPT 4.1' },
45
+ instructions: 'Translate to French',
46
+ input: '@input',
47
+ output_local: '_llm_output'
48
+ } as CallLLM
49
+ ],
50
+ exits: []
51
+ };
52
+
53
+ const formData = split_by_llm.toFormData(node);
54
+
55
+ expect(formData.uuid).to.equal('test-node');
56
+ expect(formData.llm).to.deep.equal([{ value: 'gpt-4', name: 'GPT 4.1' }]);
57
+ expect(formData.input).to.equal('@input');
58
+ expect(formData.instructions).to.equal('Translate to French');
59
+ });
60
+
61
+ it('handles empty node', () => {
62
+ const node: Node = {
63
+ uuid: 'test-node',
64
+ actions: [],
65
+ exits: []
66
+ };
67
+
68
+ const formData = split_by_llm.toFormData(node);
69
+
70
+ expect(formData.uuid).to.equal('test-node');
71
+ expect(formData.llm).to.deep.equal([]);
72
+ expect(formData.input).to.equal('@input');
73
+ expect(formData.instructions).to.equal('');
74
+ });
75
+ });
76
+
77
+ describe('fromFormData', () => {
78
+ it('creates node with call_llm action and router', () => {
79
+ const formData = {
80
+ uuid: 'test-node',
81
+ llm: [{ value: 'gpt-4', name: 'GPT 4.1' }],
82
+ input: '@input',
83
+ instructions: 'Translate to French'
84
+ };
85
+
86
+ const originalNode: Node = {
87
+ uuid: 'test-node',
88
+ actions: [],
89
+ exits: []
90
+ };
91
+
92
+ const node = split_by_llm.fromFormData(formData, originalNode);
93
+
94
+ expect(node.uuid).to.equal('test-node');
95
+ expect(node.actions).to.have.length(1);
96
+ expect(node.actions[0].type).to.equal('call_llm');
97
+ expect((node.actions[0] as CallLLM).llm).to.deep.equal({
98
+ uuid: 'gpt-4',
99
+ name: 'GPT 4.1'
100
+ });
101
+ expect((node.actions[0] as CallLLM).instructions).to.equal(
102
+ 'Translate to French'
103
+ );
104
+ expect((node.actions[0] as CallLLM).input).to.equal('@input');
105
+ expect((node.actions[0] as CallLLM).output_local).to.equal('_llm_output');
106
+
107
+ expect(node.router).to.exist;
108
+ expect(node.router.type).to.equal('switch');
109
+ expect(node.router.operand).to.equal('@locals._llm_output');
110
+ expect(node.router.categories).to.have.length(2);
111
+ expect(node.router.categories[0].name).to.equal('Success');
112
+ expect(node.router.categories[1].name).to.equal('Failure');
113
+
114
+ expect(node.exits).to.have.length(2);
115
+ });
116
+
117
+ it('handles empty form data', () => {
118
+ const formData = {
119
+ uuid: 'test-node',
120
+ llm: [],
121
+ input: '',
122
+ instructions: ''
123
+ };
124
+
125
+ const originalNode: Node = {
126
+ uuid: 'test-node',
127
+ actions: [],
128
+ exits: []
129
+ };
130
+
131
+ const node = split_by_llm.fromFormData(formData, originalNode);
132
+
133
+ const action = node.actions[0] as CallLLM;
134
+ expect(action.llm).to.deep.equal({ uuid: '', name: '' });
135
+ expect(action.instructions).to.equal('');
136
+ expect(action.input).to.equal('@input');
137
+ });
138
+ });
139
+
140
+ describe('node scenarios', () => {
141
+ const createTestNode = (
142
+ llm: any,
143
+ instructions: string,
144
+ input: string = '@input'
145
+ ): Node => ({
146
+ uuid: 'test-node',
147
+ actions: [
148
+ {
149
+ uuid: 'test-action',
150
+ type: 'call_llm',
151
+ llm,
152
+ instructions,
153
+ input,
154
+ output_local: '_llm_output'
155
+ } as CallLLM
156
+ ],
157
+ router: {
158
+ type: 'switch',
159
+ operand: '@locals._llm_output',
160
+ categories: [
161
+ {
162
+ uuid: 'success-category',
163
+ name: 'Success',
164
+ exit_uuid: 'success-exit'
165
+ },
166
+ {
167
+ uuid: 'failure-category',
168
+ name: 'Failure',
169
+ exit_uuid: 'failure-exit'
170
+ }
171
+ ],
172
+ default_category_uuid: 'failure-category',
173
+ cases: [
174
+ {
175
+ uuid: 'success-case',
176
+ type: 'has_text',
177
+ arguments: [],
178
+ category_uuid: 'success-category'
179
+ }
180
+ ]
181
+ },
182
+ exits: [
183
+ {
184
+ uuid: 'success-exit',
185
+ destination_uuid: null
186
+ },
187
+ {
188
+ uuid: 'failure-exit',
189
+ destination_uuid: null
190
+ }
191
+ ]
192
+ });
193
+
194
+ const nodeUI = {
195
+ type: 'split_by_llm',
196
+ position: { left: 50, top: 50 }
197
+ };
198
+
199
+ helper.testNode(
200
+ createTestNode({ uuid: 'gpt-4', name: 'GPT 4.1' }, 'Translate to French'),
201
+ nodeUI,
202
+ 'translation-task'
203
+ );
204
+
205
+ helper.testNode(
206
+ createTestNode(
207
+ { uuid: 'gpt-5', name: 'GPT 5' },
208
+ 'Analyze the sentiment of the following message and classify it as positive, negative, or neutral. Provide a brief explanation for your classification.'
209
+ ),
210
+ nodeUI,
211
+ 'sentiment-analysis'
212
+ );
213
+
214
+ helper.testNode(
215
+ createTestNode(
216
+ { uuid: 'gpt-4', name: 'GPT 4.1' },
217
+ 'Summarize the key points from the conversation above in bullet format.'
218
+ ),
219
+ nodeUI,
220
+ 'summarization'
221
+ );
222
+
223
+ helper.testNode(
224
+ createTestNode(
225
+ { uuid: 'gpt-5', name: 'GPT 5' },
226
+ 'Extract any contact information (phone numbers, email addresses) from the text and format them as a JSON object.'
227
+ ),
228
+ nodeUI,
229
+ 'information-extraction'
230
+ );
231
+ });
232
+ });