@nyaruka/temba-components 0.129.8 → 0.129.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (203) hide show
  1. package/CHANGELOG.md +27 -3
  2. package/demo/data/flows/sample-flow.json +186 -96
  3. package/dist/temba-components.js +414 -351
  4. package/dist/temba-components.js.map +1 -1
  5. package/out-tsc/src/events.js.map +1 -1
  6. package/out-tsc/src/excellent/helpers.js +2 -2
  7. package/out-tsc/src/excellent/helpers.js.map +1 -1
  8. package/out-tsc/src/flow/CanvasNode.js +25 -7
  9. package/out-tsc/src/flow/CanvasNode.js.map +1 -1
  10. package/out-tsc/src/flow/Editor.js +11 -1
  11. package/out-tsc/src/flow/Editor.js.map +1 -1
  12. package/out-tsc/src/flow/NodeEditor.js +133 -290
  13. package/out-tsc/src/flow/NodeEditor.js.map +1 -1
  14. package/out-tsc/src/flow/actions/add_input_labels.js +40 -0
  15. package/out-tsc/src/flow/actions/add_input_labels.js.map +1 -1
  16. package/out-tsc/src/flow/actions/call_llm.js +56 -3
  17. package/out-tsc/src/flow/actions/call_llm.js.map +1 -1
  18. package/out-tsc/src/flow/actions/call_webhook.js +1 -1
  19. package/out-tsc/src/flow/actions/call_webhook.js.map +1 -1
  20. package/out-tsc/src/flow/actions/open_ticket.js +65 -3
  21. package/out-tsc/src/flow/actions/open_ticket.js.map +1 -1
  22. package/out-tsc/src/flow/actions/set_run_result.js +75 -0
  23. package/out-tsc/src/flow/actions/set_run_result.js.map +1 -1
  24. package/out-tsc/src/flow/config.js +4 -0
  25. package/out-tsc/src/flow/config.js.map +1 -1
  26. package/out-tsc/src/flow/nodes/split_by_llm_categorize.js +227 -0
  27. package/out-tsc/src/flow/nodes/split_by_llm_categorize.js.map +1 -0
  28. package/out-tsc/src/flow/nodes/split_by_ticket.js +18 -0
  29. package/out-tsc/src/flow/nodes/split_by_ticket.js.map +1 -0
  30. package/out-tsc/src/flow/nodes/wait_for_response.js +27 -1
  31. package/out-tsc/src/flow/nodes/wait_for_response.js.map +1 -1
  32. package/out-tsc/src/flow/types.js +0 -65
  33. package/out-tsc/src/flow/types.js.map +1 -1
  34. package/out-tsc/src/form/ArrayEditor.js +18 -61
  35. package/out-tsc/src/form/ArrayEditor.js.map +1 -1
  36. package/out-tsc/src/form/FieldRenderer.js +305 -0
  37. package/out-tsc/src/form/FieldRenderer.js.map +1 -0
  38. package/out-tsc/src/form/FormField.js +3 -3
  39. package/out-tsc/src/form/FormField.js.map +1 -1
  40. package/out-tsc/src/form/TextInput.js +1 -1
  41. package/out-tsc/src/form/TextInput.js.map +1 -1
  42. package/out-tsc/src/form/select/Select.js +48 -20
  43. package/out-tsc/src/form/select/Select.js.map +1 -1
  44. package/out-tsc/src/live/ContactChat.js +39 -13
  45. package/out-tsc/src/live/ContactChat.js.map +1 -1
  46. package/out-tsc/src/markdown.js +13 -11
  47. package/out-tsc/src/markdown.js.map +1 -1
  48. package/out-tsc/test/ActionHelper.js +2 -0
  49. package/out-tsc/test/ActionHelper.js.map +1 -1
  50. package/out-tsc/test/NodeHelper.js +148 -0
  51. package/out-tsc/test/NodeHelper.js.map +1 -0
  52. package/out-tsc/test/actions/call_llm.test.js +103 -0
  53. package/out-tsc/test/actions/call_llm.test.js.map +1 -0
  54. package/out-tsc/test/nodes/split_by_llm_categorize.test.js +532 -0
  55. package/out-tsc/test/nodes/split_by_llm_categorize.test.js.map +1 -0
  56. package/out-tsc/test/nodes/split_by_random.test.js +150 -0
  57. package/out-tsc/test/nodes/split_by_random.test.js.map +1 -0
  58. package/out-tsc/test/nodes/wait_for_digits.test.js +150 -0
  59. package/out-tsc/test/nodes/wait_for_digits.test.js.map +1 -0
  60. package/out-tsc/test/nodes/wait_for_response.test.js +171 -0
  61. package/out-tsc/test/nodes/wait_for_response.test.js.map +1 -0
  62. package/out-tsc/test/temba-add-input-labels.test.js +70 -0
  63. package/out-tsc/test/temba-add-input-labels.test.js.map +1 -0
  64. package/out-tsc/test/temba-field-renderer.test.js +296 -0
  65. package/out-tsc/test/temba-field-renderer.test.js.map +1 -0
  66. package/out-tsc/test/temba-markdown.test.js +1 -1
  67. package/out-tsc/test/temba-markdown.test.js.map +1 -1
  68. package/out-tsc/test/temba-node-editor.test.js +400 -0
  69. package/out-tsc/test/temba-node-editor.test.js.map +1 -1
  70. package/out-tsc/test/temba-select.test.js +6 -3
  71. package/out-tsc/test/temba-select.test.js.map +1 -1
  72. package/out-tsc/test/temba-webchat.test.js +1 -1
  73. package/out-tsc/test/temba-webchat.test.js.map +1 -1
  74. package/package.json +1 -1
  75. package/screenshots/truth/actions/add_contact_groups/editor/descriptive-group-names.png +0 -0
  76. package/screenshots/truth/actions/add_contact_groups/editor/long-group-names.png +0 -0
  77. package/screenshots/truth/actions/add_contact_groups/editor/many-groups.png +0 -0
  78. package/screenshots/truth/actions/call_llm/editor/information-extraction.png +0 -0
  79. package/screenshots/truth/actions/call_llm/editor/sentiment-analysis.png +0 -0
  80. package/screenshots/truth/actions/call_llm/editor/summarization.png +0 -0
  81. package/screenshots/truth/actions/call_llm/editor/translation-task.png +0 -0
  82. package/screenshots/truth/actions/call_llm/render/information-extraction.png +0 -0
  83. package/screenshots/truth/actions/call_llm/render/sentiment-analysis.png +0 -0
  84. package/screenshots/truth/actions/call_llm/render/summarization.png +0 -0
  85. package/screenshots/truth/actions/call_llm/render/translation-task.png +0 -0
  86. package/screenshots/truth/actions/remove_contact_groups/editor/long-descriptive-group-names.png +0 -0
  87. package/screenshots/truth/actions/remove_contact_groups/editor/many-groups.png +0 -0
  88. package/screenshots/truth/actions/remove_contact_groups/editor/remove-from-all-groups.png +0 -0
  89. package/screenshots/truth/actions/send_email/editor/complex-business-email.png +0 -0
  90. package/screenshots/truth/actions/send_email/editor/multiple-recipients.png +0 -0
  91. package/screenshots/truth/actions/send_msg/editor/long-quick-replies.png +0 -0
  92. package/screenshots/truth/actions/send_msg/editor/multiline-text-with-replies.png +0 -0
  93. package/screenshots/truth/actions/send_msg/editor/simple-text.png +0 -0
  94. package/screenshots/truth/actions/send_msg/editor/text-with-linebreaks.png +0 -0
  95. package/screenshots/truth/actions/send_msg/editor/text-with-many-quick-replies.png +0 -0
  96. package/screenshots/truth/actions/send_msg/editor/text-with-quick-replies.png +0 -0
  97. package/screenshots/truth/actions/send_msg/editor/text-without-quick-replies.png +0 -0
  98. package/screenshots/truth/editor/router.png +0 -0
  99. package/screenshots/truth/editor/send_msg.png +0 -0
  100. package/screenshots/truth/editor/set_contact_language.png +0 -0
  101. package/screenshots/truth/editor/set_contact_name.png +0 -0
  102. package/screenshots/truth/editor/set_run_result.png +0 -0
  103. package/screenshots/truth/editor/wait.png +0 -0
  104. package/screenshots/truth/field-renderer/checkbox-checked.png +0 -0
  105. package/screenshots/truth/field-renderer/checkbox-unchecked.png +0 -0
  106. package/screenshots/truth/field-renderer/checkbox-with-errors.png +0 -0
  107. package/screenshots/truth/field-renderer/context-comparison.png +0 -0
  108. package/screenshots/truth/field-renderer/key-value-with-label.png +0 -0
  109. package/screenshots/truth/field-renderer/message-editor-with-label.png +0 -0
  110. package/screenshots/truth/field-renderer/select-multi.png +0 -0
  111. package/screenshots/truth/field-renderer/select-no-label.png +0 -0
  112. package/screenshots/truth/field-renderer/select-with-label.png +0 -0
  113. package/screenshots/truth/field-renderer/text-evaluated.png +0 -0
  114. package/screenshots/truth/field-renderer/text-no-label.png +0 -0
  115. package/screenshots/truth/field-renderer/text-with-errors.png +0 -0
  116. package/screenshots/truth/field-renderer/text-with-label.png +0 -0
  117. package/screenshots/truth/field-renderer/textarea-evaluated.png +0 -0
  118. package/screenshots/truth/field-renderer/textarea-with-label.png +0 -0
  119. package/screenshots/truth/nodes/split_by_llm_categorize/editor/basic-categorization.png +0 -0
  120. package/screenshots/truth/nodes/split_by_llm_categorize/editor/custom-input-and-result-name.png +0 -0
  121. package/screenshots/truth/nodes/split_by_llm_categorize/editor/feedback-categorization.png +0 -0
  122. package/screenshots/truth/nodes/split_by_llm_categorize/editor/many-categories.png +0 -0
  123. package/screenshots/truth/nodes/split_by_llm_categorize/editor/minimal-categories.png +0 -0
  124. package/screenshots/truth/nodes/split_by_llm_categorize/render/basic-categorization.png +0 -0
  125. package/screenshots/truth/nodes/split_by_llm_categorize/render/custom-input-and-result-name.png +0 -0
  126. package/screenshots/truth/nodes/split_by_llm_categorize/render/feedback-categorization.png +0 -0
  127. package/screenshots/truth/nodes/split_by_llm_categorize/render/many-categories.png +0 -0
  128. package/screenshots/truth/nodes/split_by_llm_categorize/render/minimal-categories.png +0 -0
  129. package/screenshots/truth/nodes/split_by_random/editor/ab-test-multiple-variants.png +0 -0
  130. package/screenshots/truth/nodes/split_by_random/editor/sampling-split.png +0 -0
  131. package/screenshots/truth/nodes/split_by_random/editor/three-way-split.png +0 -0
  132. package/screenshots/truth/nodes/split_by_random/editor/two-bucket-split.png +0 -0
  133. package/screenshots/truth/nodes/split_by_random/render/ab-test-multiple-variants.png +0 -0
  134. package/screenshots/truth/nodes/split_by_random/render/sampling-split.png +0 -0
  135. package/screenshots/truth/nodes/split_by_random/render/three-way-split.png +0 -0
  136. package/screenshots/truth/nodes/split_by_random/render/two-bucket-split.png +0 -0
  137. package/screenshots/truth/nodes/wait_for_digits/editor/basic-digits-wait.png +0 -0
  138. package/screenshots/truth/nodes/wait_for_digits/editor/phone-number-collection.png +0 -0
  139. package/screenshots/truth/nodes/wait_for_digits/editor/single-digit-with-timeout.png +0 -0
  140. package/screenshots/truth/nodes/wait_for_digits/editor/verification-code.png +0 -0
  141. package/screenshots/truth/nodes/wait_for_digits/render/basic-digits-wait.png +0 -0
  142. package/screenshots/truth/nodes/wait_for_digits/render/phone-number-collection.png +0 -0
  143. package/screenshots/truth/nodes/wait_for_digits/render/single-digit-with-timeout.png +0 -0
  144. package/screenshots/truth/nodes/wait_for_digits/render/verification-code.png +0 -0
  145. package/screenshots/truth/nodes/wait_for_response/editor/basic-wait.png +0 -0
  146. package/screenshots/truth/nodes/wait_for_response/editor/custom-result-name.png +0 -0
  147. package/screenshots/truth/nodes/wait_for_response/editor/no-timeout.png +0 -0
  148. package/screenshots/truth/nodes/wait_for_response/editor/short-timeout.png +0 -0
  149. package/screenshots/truth/nodes/wait_for_response/render/basic-wait.png +0 -0
  150. package/screenshots/truth/nodes/wait_for_response/render/custom-result-name.png +0 -0
  151. package/screenshots/truth/nodes/wait_for_response/render/no-timeout.png +0 -0
  152. package/screenshots/truth/nodes/wait_for_response/render/short-timeout.png +0 -0
  153. package/screenshots/truth/omnibox/selected.png +0 -0
  154. package/screenshots/truth/select/functions.png +0 -0
  155. package/screenshots/truth/select/multi-with-endpoint.png +0 -0
  156. package/screenshots/truth/select/search-enabled.png +0 -0
  157. package/src/events.ts +8 -1
  158. package/src/excellent/helpers.ts +2 -2
  159. package/src/flow/CanvasNode.ts +22 -1
  160. package/src/flow/Editor.ts +12 -1
  161. package/src/flow/NodeEditor.ts +186 -374
  162. package/src/flow/actions/add_input_labels.ts +45 -0
  163. package/src/flow/actions/call_llm.ts +57 -3
  164. package/src/flow/actions/call_webhook.ts +1 -1
  165. package/src/flow/actions/open_ticket.ts +74 -3
  166. package/src/flow/actions/set_run_result.ts +83 -0
  167. package/src/flow/config.ts +4 -0
  168. package/src/flow/nodes/split_by_llm_categorize.ts +277 -0
  169. package/src/flow/nodes/split_by_ticket.ts +19 -0
  170. package/src/flow/nodes/wait_for_response.ts +28 -1
  171. package/src/flow/types.ts +26 -127
  172. package/src/form/ArrayEditor.ts +34 -82
  173. package/src/form/FieldRenderer.ts +465 -0
  174. package/src/form/FormField.ts +3 -3
  175. package/src/form/TextInput.ts +1 -1
  176. package/src/form/select/Select.ts +51 -20
  177. package/src/live/ContactChat.ts +39 -15
  178. package/src/markdown.ts +19 -11
  179. package/src/store/flow-definition.d.ts +5 -2
  180. package/static/api/labels.json +31 -0
  181. package/static/api/topics.json +24 -9
  182. package/static/api/users.json +35 -16
  183. package/static/css/temba-components.css +3 -3
  184. package/stress-test.js +18 -13
  185. package/test/ActionHelper.ts +2 -0
  186. package/test/NodeHelper.ts +184 -0
  187. package/test/actions/call_llm.test.ts +137 -0
  188. package/test/nodes/README.md +78 -0
  189. package/test/nodes/split_by_llm_categorize.test.ts +698 -0
  190. package/test/nodes/split_by_random.test.ts +177 -0
  191. package/test/nodes/wait_for_digits.test.ts +176 -0
  192. package/test/nodes/wait_for_response.test.ts +206 -0
  193. package/test/temba-add-input-labels.test.ts +87 -0
  194. package/test/temba-field-renderer.test.ts +482 -0
  195. package/test/temba-markdown.test.ts +1 -1
  196. package/test/temba-node-editor.test.ts +496 -0
  197. package/test/temba-select.test.ts +6 -6
  198. package/test/temba-webchat.test.ts +1 -1
  199. package/test-assets/select/llms.json +18 -0
  200. package/web-dev-mock.mjs +96 -6
  201. package/web-dev-server.config.mjs +29 -7
  202. package/test/temba-flow-editor.test.ts.backup +0 -563
  203. package/test/temba-utils-index.test.ts.backup +0 -1737
@@ -20,6 +20,7 @@ import { ContactStoreElement } from './ContactStoreElement';
20
20
  import { Compose, ComposeValue } from '../form/Compose';
21
21
  import {
22
22
  AirtimeTransferredEvent,
23
+ CallEvent,
23
24
  ChannelEvent,
24
25
  ContactEvent,
25
26
  ContactGroupsEvent,
@@ -28,7 +29,7 @@ import {
28
29
  FlowEvent,
29
30
  MsgEvent,
30
31
  NameChangedEvent,
31
- OptinRequestedEvent,
32
+ OptInEvent,
32
33
  RunEvent,
33
34
  TicketEvent,
34
35
  UpdateFieldEvent,
@@ -54,6 +55,8 @@ export enum Events {
54
55
  MESSAGE_RECEIVED = 'msg_received',
55
56
  BROADCAST_CREATED = 'broadcast_created',
56
57
  IVR_CREATED = 'ivr_created',
58
+ CALL_CREATED = 'call_created',
59
+ CALL_RECEIVED = 'call_received',
57
60
  CONTACT_FIELD_CHANGED = 'contact_field_changed',
58
61
  CONTACT_GROUPS_CHANGED = 'contact_groups_changed',
59
62
  CONTACT_NAME_CHANGED = 'contact_name_changed',
@@ -61,7 +64,6 @@ export enum Events {
61
64
  CHANNEL_EVENT = 'channel_event',
62
65
  CONTACT_LANGUAGE_CHANGED = 'contact_language_changed',
63
66
  AIRTIME_TRANSFERRED = 'airtime_transferred',
64
- CALL_STARTED = 'call_started',
65
67
  NOTE_CREATED = 'note_created',
66
68
  TICKET_ASSIGNED = 'ticket_assigned',
67
69
  TICKET_NOTE_ADDED = 'ticket_note_added',
@@ -69,11 +71,14 @@ export enum Events {
69
71
  TICKET_OPENED = 'ticket_opened',
70
72
  TICKET_REOPENED = 'ticket_reopened',
71
73
  TICKET_TOPIC_CHANGED = 'ticket_topic_changed',
74
+ OPTIN_STARTED = 'optin_started',
75
+ OPTIN_STOPPED = 'optin_stopped',
72
76
  OPTIN_REQUESTED = 'optin_requested',
73
77
  RUN_STARTED = 'run_started',
74
78
  RUN_ENDED = 'run_ended',
75
79
 
76
80
  // deprecated
81
+ CALL_STARTED = 'call_started',
77
82
  FLOW_ENTERED = 'flow_entered',
78
83
  FLOW_EXITED = 'flow_exited'
79
84
  }
@@ -108,13 +113,13 @@ const renderChannelEvent = (event: ChannelEvent): string => {
108
113
  } else if (event.event.type === 'stop_contact') {
109
114
  return 'Stopped';
110
115
  } else if (event.event.type === 'mt_call') {
111
- return 'Outgoing Phone Call';
116
+ return 'Outgoing Phone Call'; // deprecated
112
117
  } else if (event.event.type == 'mo_call') {
113
- return 'Incoming Phone call';
118
+ return 'Incoming Phone call'; // deprecated
114
119
  } else if (event.event.type == 'optin') {
115
- return `Opted in to **${event.event.optin?.name}**`;
120
+ return `Opted in to **${event.event.optin?.name}**`; // deprecated
116
121
  } else if (event.event.type == 'optout') {
117
- return `Opted out of **${event.event.optin?.name}**`;
122
+ return `Opted out of **${event.event.optin?.name}**`; // deprecated
118
123
  }
119
124
  };
120
125
 
@@ -214,18 +219,28 @@ export const renderAirtimeTransferredEvent = (
214
219
  return `Transferred **${event.amount}** ${event.currency} of airtime`;
215
220
  };
216
221
 
217
- export const renderCallStartedEvent = (): string => {
218
- return `Call Started`;
219
- };
220
-
221
222
  export const renderContactLanguageChangedEvent = (
222
223
  event: ContactLanguageChangedEvent
223
224
  ): string => {
224
225
  return `Language updated to **${event.language}**`;
225
226
  };
226
227
 
227
- export const renderOptinRequested = (event: OptinRequestedEvent): string => {
228
- return `Requested opt-in for ${event.optin.name}`;
228
+ export const renderCallEvent = (event: CallEvent): string => {
229
+ if (event.type === Events.CALL_CREATED) {
230
+ return `Call Started`;
231
+ } else if (event.type === Events.CALL_RECEIVED) {
232
+ return `Call Answered`;
233
+ }
234
+ };
235
+
236
+ export const renderOptInEvent = (event: OptInEvent): string => {
237
+ if (event.type === Events.OPTIN_REQUESTED) {
238
+ return `Requested opt-in for ${event.optin.name}`;
239
+ } else if (event.type === Events.OPTIN_STARTED) {
240
+ return `Opted in to **${event.optin.name}**`;
241
+ } else if (event.type === Events.OPTIN_STOPPED) {
242
+ return `Opted out of **${event.optin.name}**`;
243
+ }
229
244
  };
230
245
 
231
246
  export class ContactChat extends ContactStoreElement {
@@ -694,10 +709,17 @@ export class ContactChat extends ContactStoreElement {
694
709
  text: renderAirtimeTransferredEvent(event as AirtimeTransferredEvent)
695
710
  };
696
711
  break;
697
- case Events.CALL_STARTED:
712
+ case Events.CALL_CREATED:
713
+ case Events.CALL_RECEIVED:
714
+ message = {
715
+ type: MessageType.Inline,
716
+ text: renderCallEvent(event as CallEvent)
717
+ };
718
+ break;
719
+ case Events.CALL_STARTED: // deprecated
698
720
  message = {
699
721
  type: MessageType.Inline,
700
- text: renderCallStartedEvent()
722
+ text: `Started Call`
701
723
  };
702
724
  break;
703
725
  case Events.CHANNEL_EVENT:
@@ -715,9 +737,11 @@ export class ContactChat extends ContactStoreElement {
715
737
  };
716
738
  break;
717
739
  case Events.OPTIN_REQUESTED:
740
+ case Events.OPTIN_STARTED:
741
+ case Events.OPTIN_STOPPED:
718
742
  message = {
719
743
  type: MessageType.Inline,
720
- text: renderOptinRequested(event as OptinRequestedEvent)
744
+ text: renderOptInEvent(event as OptInEvent)
721
745
  };
722
746
  break;
723
747
  }
package/src/markdown.ts CHANGED
@@ -11,31 +11,39 @@ import { Remarkable } from 'remarkable';
11
11
 
12
12
  export const markdown = new Remarkable();
13
13
 
14
- // Class-based directive API
15
- export class RenderMarkdown extends Directive {
16
- // State stored in class field
17
- // value: string | undefined;
14
+ // Base class for markdown rendering directives
15
+ abstract class BaseMarkdownDirective extends Directive {
18
16
  constructor(partInfo: PartInfo) {
19
17
  super(partInfo);
20
18
  // When necessary, validate part in constructor using `part.type`
21
19
  if (partInfo.type !== PartType.CHILD) {
22
- throw new Error('renderMarkdown only supports child expressions');
20
+ throw new Error('markdown directives only support child expressions');
23
21
  }
24
22
  }
23
+
25
24
  // Optional: override update to perform any direct DOM manipulation
26
- // DirectiveParameters<this>
27
25
  update(part: Part, [initialValue]: any) {
28
26
  /* Any imperative updates to DOM/parts would go here */
29
27
  return this.render(initialValue);
30
28
  }
31
- // Do SSR-compatible rendering (arguments are passed from call site)
29
+
30
+ // Abstract method to be implemented by subclasses
31
+ abstract render(initialValue: string): any;
32
+ }
33
+
34
+ // Class-based directive for block markdown rendering
35
+ export class RenderMarkdown extends BaseMarkdownDirective {
32
36
  render(initialValue: string) {
33
- // Previous state available on class field
34
- // if (this.value === undefined) {
35
- // this.value = initialValue;
36
- //}
37
37
  return html`${unsafeHTML(markdown.render(initialValue))}`;
38
38
  }
39
39
  }
40
40
 
41
+ // Class-based directive for inline markdown rendering
42
+ export class RenderMarkdownInline extends BaseMarkdownDirective {
43
+ render(initialValue: string) {
44
+ return html`${unsafeHTML(markdown.renderInline(initialValue))}`;
45
+ }
46
+ }
47
+
41
48
  export const renderMarkdown = directive(RenderMarkdown);
49
+ export const renderMarkdownInline = directive(RenderMarkdownInline);
@@ -44,6 +44,7 @@ export type ActionType =
44
44
  | 'split_by_subflow'
45
45
  | 'split_by_webhook'
46
46
  | 'split_by_llm'
47
+ | 'split_by_llm_categorize'
47
48
  | 'wait_for_response'
48
49
  | 'wait_for_menu'
49
50
  | 'wait_for_dial'
@@ -173,12 +174,14 @@ export interface CallResthook extends Action {
173
174
  export interface CallLLM extends Action {
174
175
  llm: NamedObject;
175
176
  instructions: string;
177
+ input: string;
176
178
  result_name: string;
177
179
  }
178
180
 
179
181
  export interface OpenTicket extends Action {
180
- subject: string;
181
- body: string;
182
+ subject?: string;
183
+ body?: string;
184
+ note?: string;
182
185
  assignee?: NamedObject;
183
186
  topic?: NamedObject;
184
187
  }
@@ -0,0 +1,31 @@
1
+ {
2
+ "next": null,
3
+ "previous": null,
4
+ "results": [
5
+ {
6
+ "uuid": "61cae99b-56e1-4f3e-a2b9-07fb5cf2be9e",
7
+ "name": "Important",
8
+ "count": 234
9
+ },
10
+ {
11
+ "uuid": "f80ed7b6-5e6a-49eb-94b6-4631a9677cd8",
12
+ "name": "Spam",
13
+ "count": 0
14
+ },
15
+ {
16
+ "uuid": "76e7a094-22ea-441d-91c4-283d8168e8e3",
17
+ "name": "Follow Up",
18
+ "count": 128
19
+ },
20
+ {
21
+ "uuid": "66e7a094-22ea-441d-91c4-283d8168e8e4",
22
+ "name": "Customer Service",
23
+ "count": 89
24
+ },
25
+ {
26
+ "uuid": "a3f2990b-4096-452e-a586-dc06a9434dde",
27
+ "name": "Feedback",
28
+ "count": 42
29
+ }
30
+ ]
31
+ }
@@ -3,19 +3,34 @@
3
3
  "previous": null,
4
4
  "results": [
5
5
  {
6
- "uuid": "topic-1",
7
- "name": "General Support",
8
- "created_on": "2024-01-01T00:00:00Z"
6
+ "uuid": "1b1cb507-e079-4b30-818c-1898edcbd178",
7
+ "name": "General",
8
+ "counts": {
9
+ "open": 2,
10
+ "closed": 12
11
+ },
12
+ "system": true,
13
+ "created_on": "2021-08-25T22:50:51.381947Z"
9
14
  },
10
15
  {
11
- "uuid": "topic-2",
12
- "name": "Technical Issues",
13
- "created_on": "2024-01-01T00:00:00Z"
16
+ "uuid": "bf4b568d-97b8-4d20-aed5-ad8150270af8",
17
+ "name": "Technical Support",
18
+ "counts": {
19
+ "open": 5,
20
+ "closed": 23
21
+ },
22
+ "system": false,
23
+ "created_on": "2021-08-25T22:51:15.123456Z"
14
24
  },
15
25
  {
16
- "uuid": "topic-3",
17
- "name": "Billing Questions",
18
- "created_on": "2024-01-01T00:00:00Z"
26
+ "uuid": "a3f7e9d2-1c8b-4e5f-9a6b-7d4c2e8f1a3b",
27
+ "name": "Billing",
28
+ "counts": {
29
+ "open": 1,
30
+ "closed": 8
31
+ },
32
+ "system": false,
33
+ "created_on": "2021-08-25T22:52:30.789012Z"
19
34
  }
20
35
  ]
21
36
  }
@@ -3,24 +3,43 @@
3
3
  "previous": null,
4
4
  "results": [
5
5
  {
6
- "uuid": "user-1",
7
- "email": "admin@example.com",
8
- "first_name": "Admin",
9
- "last_name": "User",
10
- "username": "admin",
11
- "is_active": true,
12
- "is_staff": true,
13
- "date_joined": "2024-01-01T00:00:00Z"
6
+ "uuid": "c0f5b431-35e9-429c-9d57-5fee2fac46a3",
7
+ "email": "eric+marion+berry@textit.com",
8
+ "first_name": "Marion",
9
+ "last_name": "Berry",
10
+ "name": "Marion Berry",
11
+ "role": "agent",
12
+ "team": {
13
+ "uuid": "15236c1e-9375-4f84-bb48-ec64283d1eb9",
14
+ "name": "All Topics"
15
+ },
16
+ "created_on": "2023-04-05T21:11:31.909765Z",
17
+ "avatar": null
14
18
  },
15
19
  {
16
- "uuid": "user-2",
17
- "email": "editor@example.com",
18
- "first_name": "Editor",
19
- "last_name": "User",
20
- "username": "editor",
21
- "is_active": true,
22
- "is_staff": false,
23
- "date_joined": "2024-01-01T00:00:00Z"
20
+ "uuid": "ae79dd5b-8c34-4602-a2c1-1e4db2419f0f",
21
+ "email": "eric@textit.com",
22
+ "first_name": "Eric",
23
+ "last_name": "Newcomer",
24
+ "name": "Eric Newcomer",
25
+ "role": "administrator",
26
+ "team": null,
27
+ "created_on": "2013-02-26T21:19:44Z",
28
+ "avatar": "https://dl-textit.s3.amazonaws.com/avatars/4/b6d756224c61435bb36b57ae03b83359.jpg"
29
+ },
30
+ {
31
+ "uuid": "f8e2a1b5-9c7d-4e6f-8a3b-1c5e9f2d4a6b",
32
+ "email": "sarah.smith@textit.com",
33
+ "first_name": "Sarah",
34
+ "last_name": "Smith",
35
+ "name": "Sarah Smith",
36
+ "role": "agent",
37
+ "team": {
38
+ "uuid": "15236c1e-9375-4f84-bb48-ec64283d1eb9",
39
+ "name": "All Topics"
40
+ },
41
+ "created_on": "2023-06-15T14:22:18.456789Z",
42
+ "avatar": null
24
43
  }
25
44
  ]
26
45
  }
@@ -61,7 +61,7 @@
61
61
  --color-text-light: rgba(255, 255, 255, 1);
62
62
  --color-text-dark: rgba(0, 0, 0, 0.8);
63
63
  --color-text-dark-secondary: rgba(0, 0, 0, 0.25);
64
- --color-text-help: rgba(0, 0, 0, 0.35);
64
+ --color-text-help: rgb(120, 120, 120);
65
65
  --color-tertiary: rgb(var(--tertiary-rgb));
66
66
 
67
67
  --help-text-size: 0.85em;
@@ -125,8 +125,8 @@
125
125
  --header-bg: var(--color-primary-dark);
126
126
  --header-text: var(--color-text-light);
127
127
 
128
- --temba-textinput-padding: 9px;
129
- --temba-textinput-font-size: 13px;
128
+ --temba-textinput-padding: 9px 14px;
129
+ --temba-textinput-font-size: 14px;
130
130
 
131
131
  --options-block-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.03);
132
132
  --options-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
package/stress-test.js CHANGED
@@ -11,7 +11,7 @@ let maxRuns = 10;
11
11
  // Parse arguments
12
12
  for (let i = 0; i < args.length; i++) {
13
13
  const arg = args[i];
14
-
14
+
15
15
  if (arg.startsWith('--runs=')) {
16
16
  maxRuns = parseInt(arg.split('=')[1]);
17
17
  if (isNaN(maxRuns) || maxRuns <= 0) {
@@ -28,12 +28,16 @@ for (let i = 0; i < args.length; i++) {
28
28
  // Validate test file
29
29
  if (!testFile) {
30
30
  console.error('❌ Usage: yarn stress-test <test-file> [--runs=N]');
31
- console.error(' Example: yarn stress-test test/temba-webchat.test.ts --runs=100');
31
+ console.error(
32
+ ' Example: yarn stress-test test/temba-webchat.test.ts --runs=100'
33
+ );
32
34
  process.exit(1);
33
35
  }
34
36
 
35
37
  if (!testFile.startsWith('test/') || !testFile.endsWith('.test.ts')) {
36
- console.error('❌ Test file must be in the test/ directory and end with .test.ts');
38
+ console.error(
39
+ '❌ Test file must be in the test/ directory and end with .test.ts'
40
+ );
37
41
  process.exit(1);
38
42
  }
39
43
 
@@ -51,21 +55,21 @@ const startTime = performance.now();
51
55
  try {
52
56
  while (run <= maxRuns) {
53
57
  const runStartTime = performance.now();
54
-
58
+
55
59
  process.stdout.write(`Run ${run.toString().padStart(3)}/${maxRuns}: `);
56
-
60
+
57
61
  try {
58
62
  // Run the test with minimal output
59
- const result = execSync(`yarn test ${testFile}`, {
63
+ const result = execSync(`yarn test ${testFile}`, {
60
64
  encoding: 'utf-8',
61
65
  stdio: ['pipe', 'pipe', 'pipe']
62
66
  });
63
-
67
+
64
68
  const runEndTime = performance.now();
65
69
  const runTime = runEndTime - runStartTime;
66
70
  runTimes.push(runTime);
67
71
  totalTime += runTime;
68
-
72
+
69
73
  // Check if the test actually passed by looking for success indicators
70
74
  if (result.includes('all tests passed') || result.includes('0 failed')) {
71
75
  console.log(`✅ PASS (${(runTime / 1000).toFixed(2)}s)`);
@@ -75,11 +79,10 @@ try {
75
79
  failures++;
76
80
  break;
77
81
  }
78
-
79
82
  } catch (error) {
80
83
  const runEndTime = performance.now();
81
84
  const runTime = runEndTime - runStartTime;
82
-
85
+
83
86
  console.log(`❌ FAIL (${(runTime / 1000).toFixed(2)}s)`);
84
87
  console.log('');
85
88
  console.log('💥 Test failed on run', run);
@@ -94,7 +97,7 @@ try {
94
97
  failures++;
95
98
  break;
96
99
  }
97
-
100
+
98
101
  run++;
99
102
  }
100
103
  } catch (error) {
@@ -112,14 +115,16 @@ console.log('==================');
112
115
  console.log(`Test file: ${testFile}`);
113
116
  console.log(`Completed runs: ${run - 1}/${maxRuns}`);
114
117
  console.log(`Failures: ${failures}`);
115
- console.log(`Success rate: ${(((run - 1 - failures) / (run - 1)) * 100).toFixed(1)}%`);
118
+ console.log(
119
+ `Success rate: ${(((run - 1 - failures) / (run - 1)) * 100).toFixed(1)}%`
120
+ );
116
121
  console.log('');
117
122
 
118
123
  if (runTimes.length > 0) {
119
124
  const avgTime = runTimes.reduce((a, b) => a + b, 0) / runTimes.length;
120
125
  const minTime = Math.min(...runTimes);
121
126
  const maxTime = Math.max(...runTimes);
122
-
127
+
123
128
  console.log('⏱️ Timing Statistics');
124
129
  console.log('=====================');
125
130
  console.log(`Total time: ${(totalTestTime / 1000).toFixed(2)}s`);
@@ -8,6 +8,8 @@ import '../temba-modules';
8
8
  /**
9
9
  * Generic action test framework
10
10
  * Tests the complete action lifecycle: render → edit → save → validate
11
+ *
12
+ * For node configuration testing, see NodeHelper.ts
11
13
  */
12
14
  export class ActionTest<T extends Action> {
13
15
  constructor(private actionConfig: any, private actionName: string) {}
@@ -0,0 +1,184 @@
1
+ import { fixture, expect } from '@open-wc/testing';
2
+ import { html } from 'lit';
3
+ import { Node } from '../src/store/flow-definition';
4
+ import { assertScreenshot, getClip } from './utils.test';
5
+ import { Editor } from '../src/flow/Editor';
6
+ import '../temba-modules';
7
+
8
+ /**
9
+ * Generic node test framework
10
+ * Tests the complete node lifecycle: render → edit → save → validate
11
+ *
12
+ * This is the node configuration equivalent of ActionHelper.ts for action configurations.
13
+ * It provides uniform testing for all types of nodes: simple wait nodes, router-based
14
+ * split nodes, and complex form-configured nodes.
15
+ */
16
+ export class NodeTest<T extends Node> {
17
+ constructor(private nodeConfig: any, private nodeName: string) {}
18
+
19
+ /**
20
+ * Renders a node in the flow editor and returns the flow node
21
+ */
22
+ private async renderNode(node: T, nodeUI: any): Promise<HTMLElement> {
23
+ const mockDefinition = {
24
+ nodes: [node],
25
+ _ui: {
26
+ nodes: {
27
+ [node.uuid]: {
28
+ type: nodeUI.type,
29
+ position: { left: 50, top: 50 },
30
+ ...nodeUI
31
+ }
32
+ }
33
+ }
34
+ };
35
+
36
+ const editor = (await fixture(html`
37
+ <temba-flow-editor>
38
+ <div id="canvas"></div>
39
+ </temba-flow-editor>
40
+ `)) as Editor;
41
+
42
+ (editor as any).definition = mockDefinition;
43
+ (editor as any).canvasSize = { width: 400, height: 300 };
44
+ await editor.updateComplete;
45
+
46
+ const flowNode = editor.querySelector('temba-flow-node') as HTMLElement;
47
+ expect(flowNode).to.exist;
48
+
49
+ return flowNode;
50
+ }
51
+
52
+ /**
53
+ * Opens the node editor for a node and returns the editor element
54
+ */
55
+ private async openNodeEditor(node: T, nodeUI: any): Promise<HTMLElement> {
56
+ const nodeEditor = (await fixture(html`
57
+ <temba-node-editor
58
+ .node=${node}
59
+ .nodeUI=${nodeUI}
60
+ .isOpen=${true}
61
+ ></temba-node-editor>
62
+ `)) as HTMLElement;
63
+
64
+ await (nodeEditor as any).updateComplete;
65
+
66
+ // Wait for form data initialization if needed
67
+ await new Promise((resolve) => setTimeout(resolve, 200));
68
+ await (nodeEditor as any).updateComplete;
69
+
70
+ expect(nodeEditor).to.exist;
71
+
72
+ return nodeEditor;
73
+ }
74
+
75
+ /**
76
+ * Takes a screenshot of the dialog container within a node editor
77
+ */
78
+ private async assertDialogScreenshot(
79
+ el: HTMLElement,
80
+ screenshotName: string
81
+ ) {
82
+ const dialog = el.shadowRoot
83
+ ?.querySelector('temba-dialog')
84
+ ?.shadowRoot?.querySelector('.dialog-container') as HTMLElement;
85
+ await assertScreenshot(screenshotName, getClip(dialog));
86
+ }
87
+
88
+ /**
89
+ * Complete test for a node configuration
90
+ * 1. Renders the node in a flow node (with screenshot)
91
+ * 2. Opens the node editor (with screenshot)
92
+ * 3. Simulates save and validates round-trip conversion
93
+ */
94
+ async testNode(node: T, nodeUI: any, testName: string) {
95
+ it(`${testName}`, async () => {
96
+ // Step 1: Render node in flow node
97
+ const flowNode = await this.renderNode(node, nodeUI);
98
+
99
+ // For execute_actions nodes, check for .body, for router nodes check for .router or .categories
100
+ const hasContent =
101
+ flowNode.querySelector('.body') ||
102
+ flowNode.querySelector('.router') ||
103
+ flowNode.querySelector('.categories') ||
104
+ flowNode.querySelector('.action') ||
105
+ flowNode.textContent?.trim();
106
+
107
+ expect(hasContent).to.exist;
108
+ await assertScreenshot(
109
+ `nodes/${this.nodeName}/render/${testName}`,
110
+ getClip(flowNode)
111
+ );
112
+
113
+ // Step 2: Open node editor
114
+ const nodeEditor = await this.openNodeEditor(node, nodeUI);
115
+ await this.assertDialogScreenshot(
116
+ nodeEditor,
117
+ `nodes/${this.nodeName}/editor/${testName}`
118
+ );
119
+
120
+ // Step 3: Test round-trip conversion (simulates save workflow)
121
+ if (this.nodeConfig.toFormData && this.nodeConfig.fromFormData) {
122
+ const formData = this.nodeConfig.toFormData(node);
123
+ const convertedNode = this.nodeConfig.fromFormData(formData, node) as T;
124
+
125
+ // Validate the round trip worked
126
+ expect(convertedNode.uuid).to.equal(node.uuid);
127
+
128
+ // Validate the converted node has expected structure
129
+ expect(convertedNode).to.have.property('actions');
130
+ expect(convertedNode).to.have.property('exits');
131
+
132
+ expect(convertedNode).to.deep.equal(node);
133
+ }
134
+ });
135
+ }
136
+
137
+ /**
138
+ * Run basic property tests
139
+ */
140
+ testBasicProperties() {
141
+ it('has correct basic properties', () => {
142
+ expect(this.nodeConfig.type).to.be.a('string');
143
+
144
+ // Name is optional - only some node configs have it
145
+ if (this.nodeConfig.name) {
146
+ expect(this.nodeConfig.name).to.be.a('string');
147
+ }
148
+
149
+ // Color is optional
150
+ if (this.nodeConfig.color) {
151
+ expect(this.nodeConfig.color).to.be.a('string');
152
+ }
153
+
154
+ // toFormData and fromFormData are optional - only needed for complex data transformations
155
+ if (this.nodeConfig.toFormData) {
156
+ expect(this.nodeConfig.toFormData).to.be.a('function');
157
+ }
158
+ if (this.nodeConfig.fromFormData) {
159
+ expect(this.nodeConfig.fromFormData).to.be.a('function');
160
+ }
161
+
162
+ // Form configuration is optional
163
+ if (this.nodeConfig.form) {
164
+ expect(this.nodeConfig.form).to.be.an('object');
165
+ }
166
+
167
+ // Layout is optional
168
+ if (this.nodeConfig.layout) {
169
+ expect(this.nodeConfig.layout).to.be.an('array');
170
+ }
171
+
172
+ // Router config is optional
173
+ if (this.nodeConfig.router) {
174
+ expect(this.nodeConfig.router).to.be.an('object');
175
+ expect(this.nodeConfig.router.type).to.exist;
176
+ }
177
+
178
+ // Render function is optional
179
+ if (this.nodeConfig.render) {
180
+ expect(this.nodeConfig.render).to.be.a('function');
181
+ }
182
+ });
183
+ }
184
+ }