@servicetitan/titan-chat-ui 1.0.1

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 (244) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/assets/floating-chat-avatar.svg +16 -0
  3. package/dist/components/chat/__tests-cy__/chat-messages.test.d.ts +2 -0
  4. package/dist/components/chat/__tests-cy__/chat-messages.test.d.ts.map +1 -0
  5. package/dist/components/chat/__tests-cy__/chat-messages.test.js +28 -0
  6. package/dist/components/chat/__tests-cy__/chat-messages.test.js.map +1 -0
  7. package/dist/components/chat/__tests-cy__/chat.test.d.ts +2 -0
  8. package/dist/components/chat/__tests-cy__/chat.test.d.ts.map +1 -0
  9. package/dist/components/chat/__tests-cy__/chat.test.js +122 -0
  10. package/dist/components/chat/__tests-cy__/chat.test.js.map +1 -0
  11. package/dist/components/chat/chat-connecting.d.ts +7 -0
  12. package/dist/components/chat/chat-connecting.d.ts.map +1 -0
  13. package/dist/components/chat/chat-connecting.js +6 -0
  14. package/dist/components/chat/chat-connecting.js.map +1 -0
  15. package/dist/components/chat/chat-error.d.ts +3 -0
  16. package/dist/components/chat/chat-error.d.ts.map +1 -0
  17. package/dist/components/chat/chat-error.js +18 -0
  18. package/dist/components/chat/chat-error.js.map +1 -0
  19. package/dist/components/chat/chat-error.module.less +6 -0
  20. package/dist/components/chat/chat-input-file.d.ts +5 -0
  21. package/dist/components/chat/chat-input-file.d.ts.map +1 -0
  22. package/dist/components/chat/chat-input-file.js +43 -0
  23. package/dist/components/chat/chat-input-file.js.map +1 -0
  24. package/dist/components/chat/chat-input.d.ts +5 -0
  25. package/dist/components/chat/chat-input.d.ts.map +1 -0
  26. package/dist/components/chat/chat-input.js +94 -0
  27. package/dist/components/chat/chat-input.js.map +1 -0
  28. package/dist/components/chat/chat-input.module.less +11 -0
  29. package/dist/components/chat/chat-message-template-agent.d.ts +4 -0
  30. package/dist/components/chat/chat-message-template-agent.d.ts.map +1 -0
  31. package/dist/components/chat/chat-message-template-agent.js +14 -0
  32. package/dist/components/chat/chat-message-template-agent.js.map +1 -0
  33. package/dist/components/chat/chat-message-template-user.d.ts +4 -0
  34. package/dist/components/chat/chat-message-template-user.d.ts.map +1 -0
  35. package/dist/components/chat/chat-message-template-user.js +16 -0
  36. package/dist/components/chat/chat-message-template-user.js.map +1 -0
  37. package/dist/components/chat/chat-message-typing.d.ts +3 -0
  38. package/dist/components/chat/chat-message-typing.d.ts.map +1 -0
  39. package/dist/components/chat/chat-message-typing.js +15 -0
  40. package/dist/components/chat/chat-message-typing.js.map +1 -0
  41. package/dist/components/chat/chat-message.d.ts +12 -0
  42. package/dist/components/chat/chat-message.d.ts.map +1 -0
  43. package/dist/components/chat/chat-message.js +60 -0
  44. package/dist/components/chat/chat-message.js.map +1 -0
  45. package/dist/components/chat/chat-messages.d.ts +7 -0
  46. package/dist/components/chat/chat-messages.d.ts.map +1 -0
  47. package/dist/components/chat/chat-messages.js +12 -0
  48. package/dist/components/chat/chat-messages.js.map +1 -0
  49. package/dist/components/chat/chat-notifications.d.ts +5 -0
  50. package/dist/components/chat/chat-notifications.d.ts.map +1 -0
  51. package/dist/components/chat/chat-notifications.js +12 -0
  52. package/dist/components/chat/chat-notifications.js.map +1 -0
  53. package/dist/components/chat/chat-timer.d.ts +3 -0
  54. package/dist/components/chat/chat-timer.d.ts.map +1 -0
  55. package/dist/components/chat/chat-timer.js +18 -0
  56. package/dist/components/chat/chat-timer.js.map +1 -0
  57. package/dist/components/chat/chat-timer.module.less +5 -0
  58. package/dist/components/chat/chat.d.ts +8 -0
  59. package/dist/components/chat/chat.d.ts.map +1 -0
  60. package/dist/components/chat/chat.js +28 -0
  61. package/dist/components/chat/chat.js.map +1 -0
  62. package/dist/components/common/multiline-text.d.ts +8 -0
  63. package/dist/components/common/multiline-text.d.ts.map +1 -0
  64. package/dist/components/common/multiline-text.js +12 -0
  65. package/dist/components/common/multiline-text.js.map +1 -0
  66. package/dist/components/common/multiline-text.module.less +9 -0
  67. package/dist/components/message-content/message-content-file.d.ts +8 -0
  68. package/dist/components/message-content/message-content-file.d.ts.map +1 -0
  69. package/dist/components/message-content/message-content-file.js +11 -0
  70. package/dist/components/message-content/message-content-file.js.map +1 -0
  71. package/dist/components/message-content/message-content-text.d.ts +8 -0
  72. package/dist/components/message-content/message-content-text.d.ts.map +1 -0
  73. package/dist/components/message-content/message-content-text.js +7 -0
  74. package/dist/components/message-content/message-content-text.js.map +1 -0
  75. package/dist/components/messages/__tests-cy__/message-agent.test.d.ts +2 -0
  76. package/dist/components/messages/__tests-cy__/message-agent.test.d.ts.map +1 -0
  77. package/dist/components/messages/__tests-cy__/message-agent.test.js +89 -0
  78. package/dist/components/messages/__tests-cy__/message-agent.test.js.map +1 -0
  79. package/dist/components/messages/__tests-cy__/message-system.test.d.ts +2 -0
  80. package/dist/components/messages/__tests-cy__/message-system.test.d.ts.map +1 -0
  81. package/dist/components/messages/__tests-cy__/message-system.test.js +20 -0
  82. package/dist/components/messages/__tests-cy__/message-system.test.js.map +1 -0
  83. package/dist/components/messages/__tests-cy__/message-timeout.test.d.ts +2 -0
  84. package/dist/components/messages/__tests-cy__/message-timeout.test.d.ts.map +1 -0
  85. package/dist/components/messages/__tests-cy__/message-timeout.test.js +32 -0
  86. package/dist/components/messages/__tests-cy__/message-timeout.test.js.map +1 -0
  87. package/dist/components/messages/__tests-cy__/message-typing.test.d.ts +2 -0
  88. package/dist/components/messages/__tests-cy__/message-typing.test.d.ts.map +1 -0
  89. package/dist/components/messages/__tests-cy__/message-typing.test.js +49 -0
  90. package/dist/components/messages/__tests-cy__/message-typing.test.js.map +1 -0
  91. package/dist/components/messages/__tests-cy__/message-user.test.d.ts +2 -0
  92. package/dist/components/messages/__tests-cy__/message-user.test.d.ts.map +1 -0
  93. package/dist/components/messages/__tests-cy__/message-user.test.js +33 -0
  94. package/dist/components/messages/__tests-cy__/message-user.test.js.map +1 -0
  95. package/dist/components/messages/message-agent.d.ts +12 -0
  96. package/dist/components/messages/message-agent.d.ts.map +1 -0
  97. package/dist/components/messages/message-agent.js +18 -0
  98. package/dist/components/messages/message-agent.js.map +1 -0
  99. package/dist/components/messages/message-agent.module.less +59 -0
  100. package/dist/components/messages/message-avatar.d.ts +9 -0
  101. package/dist/components/messages/message-avatar.d.ts.map +1 -0
  102. package/dist/components/messages/message-avatar.js +14 -0
  103. package/dist/components/messages/message-avatar.js.map +1 -0
  104. package/dist/components/messages/message-avatar.module.less +26 -0
  105. package/dist/components/messages/message-footer.d.ts +7 -0
  106. package/dist/components/messages/message-footer.d.ts.map +1 -0
  107. package/dist/components/messages/message-footer.js +7 -0
  108. package/dist/components/messages/message-footer.js.map +1 -0
  109. package/dist/components/messages/message-system.d.ts +8 -0
  110. package/dist/components/messages/message-system.d.ts.map +1 -0
  111. package/dist/components/messages/message-system.js +12 -0
  112. package/dist/components/messages/message-system.js.map +1 -0
  113. package/dist/components/messages/message-system.module.less +26 -0
  114. package/dist/components/messages/message-timeout.d.ts +8 -0
  115. package/dist/components/messages/message-timeout.d.ts.map +1 -0
  116. package/dist/components/messages/message-timeout.js +16 -0
  117. package/dist/components/messages/message-timeout.js.map +1 -0
  118. package/dist/components/messages/message-typing.d.ts +8 -0
  119. package/dist/components/messages/message-typing.d.ts.map +1 -0
  120. package/dist/components/messages/message-typing.js +10 -0
  121. package/dist/components/messages/message-typing.js.map +1 -0
  122. package/dist/components/messages/message-typing.module.less +40 -0
  123. package/dist/components/messages/message-user.d.ts +9 -0
  124. package/dist/components/messages/message-user.d.ts.map +1 -0
  125. package/dist/components/messages/message-user.js +13 -0
  126. package/dist/components/messages/message-user.js.map +1 -0
  127. package/dist/components/messages/message-user.module.less +35 -0
  128. package/dist/components/messages/use-avatar-props.d.ts +4 -0
  129. package/dist/components/messages/use-avatar-props.d.ts.map +1 -0
  130. package/dist/components/messages/use-avatar-props.js +12 -0
  131. package/dist/components/messages/use-avatar-props.js.map +1 -0
  132. package/dist/index.d.ts +12 -0
  133. package/dist/index.d.ts.map +1 -0
  134. package/dist/index.js +12 -0
  135. package/dist/index.js.map +1 -0
  136. package/dist/models/chat-customizations.d.ts +29 -0
  137. package/dist/models/chat-customizations.d.ts.map +1 -0
  138. package/dist/models/chat-customizations.js +2 -0
  139. package/dist/models/chat-customizations.js.map +1 -0
  140. package/dist/models/component.d.ts +4 -0
  141. package/dist/models/component.d.ts.map +1 -0
  142. package/dist/models/component.js +2 -0
  143. package/dist/models/component.js.map +1 -0
  144. package/dist/models/index.d.ts +3 -0
  145. package/dist/models/index.d.ts.map +1 -0
  146. package/dist/models/index.js +3 -0
  147. package/dist/models/index.js.map +1 -0
  148. package/dist/models/support-chat.d.ts +66 -0
  149. package/dist/models/support-chat.d.ts.map +1 -0
  150. package/dist/models/support-chat.js +28 -0
  151. package/dist/models/support-chat.js.map +1 -0
  152. package/dist/stores/__mocks-cy__/chat-ui.store.mock.d.ts +65 -0
  153. package/dist/stores/__mocks-cy__/chat-ui.store.mock.d.ts.map +1 -0
  154. package/dist/stores/__mocks-cy__/chat-ui.store.mock.js +268 -0
  155. package/dist/stores/__mocks-cy__/chat-ui.store.mock.js.map +1 -0
  156. package/dist/stores/chat-input.store.d.ts +10 -0
  157. package/dist/stores/chat-input.store.d.ts.map +1 -0
  158. package/dist/stores/chat-input.store.js +46 -0
  159. package/dist/stores/chat-input.store.js.map +1 -0
  160. package/dist/stores/chat-ui-backend-echo.store.d.ts +13 -0
  161. package/dist/stores/chat-ui-backend-echo.store.d.ts.map +1 -0
  162. package/dist/stores/chat-ui-backend-echo.store.js +111 -0
  163. package/dist/stores/chat-ui-backend-echo.store.js.map +1 -0
  164. package/dist/stores/chat-ui-backend.store.d.ts +6 -0
  165. package/dist/stores/chat-ui-backend.store.d.ts.map +1 -0
  166. package/dist/stores/chat-ui-backend.store.js +3 -0
  167. package/dist/stores/chat-ui-backend.store.js.map +1 -0
  168. package/dist/stores/chat-ui.store.d.ts +142 -0
  169. package/dist/stores/chat-ui.store.d.ts.map +1 -0
  170. package/dist/stores/chat-ui.store.js +679 -0
  171. package/dist/stores/chat-ui.store.js.map +1 -0
  172. package/dist/stores/index.d.ts +3 -0
  173. package/dist/stores/index.d.ts.map +1 -0
  174. package/dist/stores/index.js +3 -0
  175. package/dist/stores/index.js.map +1 -0
  176. package/dist/utils/text-utils.d.ts +11 -0
  177. package/dist/utils/text-utils.d.ts.map +1 -0
  178. package/dist/utils/text-utils.js +82 -0
  179. package/dist/utils/text-utils.js.map +1 -0
  180. package/package.json +52 -0
  181. package/src/assets/floating-chat-avatar.svg +16 -0
  182. package/src/components/chat/__tests-cy__/chat-messages.test.tsx +36 -0
  183. package/src/components/chat/__tests-cy__/chat.test.tsx +156 -0
  184. package/src/components/chat/chat-connecting.tsx +23 -0
  185. package/src/components/chat/chat-error.module.less +6 -0
  186. package/src/components/chat/chat-error.module.less.d.ts +3 -0
  187. package/src/components/chat/chat-error.tsx +39 -0
  188. package/src/components/chat/chat-input-file.tsx +68 -0
  189. package/src/components/chat/chat-input.module.less +11 -0
  190. package/src/components/chat/chat-input.module.less.d.ts +3 -0
  191. package/src/components/chat/chat-input.tsx +143 -0
  192. package/src/components/chat/chat-message-template-agent.tsx +26 -0
  193. package/src/components/chat/chat-message-template-user.tsx +46 -0
  194. package/src/components/chat/chat-message-typing.tsx +19 -0
  195. package/src/components/chat/chat-message.tsx +78 -0
  196. package/src/components/chat/chat-messages.tsx +23 -0
  197. package/src/components/chat/chat-notifications.tsx +19 -0
  198. package/src/components/chat/chat-timer.module.less +5 -0
  199. package/src/components/chat/chat-timer.module.less.d.ts +3 -0
  200. package/src/components/chat/chat-timer.tsx +35 -0
  201. package/src/components/chat/chat.tsx +55 -0
  202. package/src/components/common/multiline-text.module.less +9 -0
  203. package/src/components/common/multiline-text.module.less.d.ts +3 -0
  204. package/src/components/common/multiline-text.tsx +30 -0
  205. package/src/components/message-content/message-content-file.tsx +27 -0
  206. package/src/components/message-content/message-content-text.tsx +12 -0
  207. package/src/components/messages/__tests-cy__/message-agent.test.tsx +155 -0
  208. package/src/components/messages/__tests-cy__/message-system.test.tsx +33 -0
  209. package/src/components/messages/__tests-cy__/message-timeout.test.tsx +38 -0
  210. package/src/components/messages/__tests-cy__/message-typing.test.tsx +58 -0
  211. package/src/components/messages/__tests-cy__/message-user.test.tsx +52 -0
  212. package/src/components/messages/message-agent.module.less +59 -0
  213. package/src/components/messages/message-agent.module.less.d.ts +9 -0
  214. package/src/components/messages/message-agent.tsx +62 -0
  215. package/src/components/messages/message-avatar.module.less +26 -0
  216. package/src/components/messages/message-avatar.module.less.d.ts +5 -0
  217. package/src/components/messages/message-avatar.tsx +33 -0
  218. package/src/components/messages/message-footer.tsx +17 -0
  219. package/src/components/messages/message-system.module.less +26 -0
  220. package/src/components/messages/message-system.module.less.d.ts +5 -0
  221. package/src/components/messages/message-system.tsx +35 -0
  222. package/src/components/messages/message-timeout.tsx +42 -0
  223. package/src/components/messages/message-typing.module.less +40 -0
  224. package/src/components/messages/message-typing.module.less.d.ts +5 -0
  225. package/src/components/messages/message-typing.tsx +25 -0
  226. package/src/components/messages/message-user.module.less +35 -0
  227. package/src/components/messages/message-user.module.less.d.ts +7 -0
  228. package/src/components/messages/message-user.tsx +49 -0
  229. package/src/components/messages/use-avatar-props.tsx +17 -0
  230. package/src/cypress.d.ts +10 -0
  231. package/src/index.ts +11 -0
  232. package/src/models/chat-customizations.ts +34 -0
  233. package/src/models/component.ts +3 -0
  234. package/src/models/index.ts +2 -0
  235. package/src/models/support-chat.ts +84 -0
  236. package/src/stores/__mocks-cy__/chat-ui.store.mock.ts +105 -0
  237. package/src/stores/chat-input.store.ts +25 -0
  238. package/src/stores/chat-ui-backend-echo.store.ts +94 -0
  239. package/src/stores/chat-ui-backend.store.ts +10 -0
  240. package/src/stores/chat-ui.store.ts +537 -0
  241. package/src/stores/index.ts +10 -0
  242. package/src/utils/text-utils.ts +93 -0
  243. package/tsconfig.json +15 -0
  244. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,537 @@
1
+ import { EventEmitter } from 'events';
2
+ import { FileDescriptor } from '@servicetitan/form';
3
+ import { injectable, symbolToken } from '@servicetitan/react-ioc';
4
+ import { action, computed, makeObservable, observable, runInAction } from 'mobx';
5
+ import { nanoid } from 'nanoid';
6
+ import {
7
+ ChatConfiguration,
8
+ ChatCustomizations,
9
+ ChatEndReason,
10
+ ChatMessageModelBase,
11
+ ChatMessageModelFile,
12
+ ChatMessageModelText,
13
+ ChatMessageModelTimeout,
14
+ ChatMessageModelType,
15
+ ChatMessageModelWelcome,
16
+ ChatMessageState,
17
+ ChatParticipantIcon,
18
+ ChatParticipantModel,
19
+ ChatRunState,
20
+ ChatTimer,
21
+ ChatUiStateError,
22
+ } from '../models';
23
+
24
+ export const symbolAgent = Symbol('SupportChatAgent');
25
+ export const symbolUser = Symbol('SupportChatUser');
26
+
27
+ export enum ChatUiEvent {
28
+ eventRun = 'eventRun',
29
+ eventDestroy = 'eventDestroy',
30
+ eventRestart = 'eventRestart',
31
+ eventRecover = 'eventRecover',
32
+ eventChasitorTyping = 'eventChasitorTyping',
33
+ eventTimerRestart = 'eventTimerRestart',
34
+ eventMessageSend = 'eventMessageSend',
35
+ eventMessageSendFile = 'eventMessageSendFile',
36
+ eventMessageSendRetry = 'eventMessageSendRetry',
37
+ }
38
+
39
+ export type ChatUiEventListener<T = void> = (
40
+ resolve: (value: T | PromiseLike<T>) => void,
41
+ reject: (reason?: any) => void,
42
+ ...args: unknown[]
43
+ ) => void;
44
+
45
+ export interface IChatUiStore<T extends ChatCustomizations = ChatCustomizations> {
46
+ scrollCounter: number;
47
+
48
+ status: ChatRunState;
49
+ get isStarting(): boolean;
50
+ get isStarted(): boolean;
51
+ get isError(): boolean;
52
+ get isEnded(): boolean;
53
+ get customizations(): T;
54
+ timer?: ChatTimer;
55
+ endReason?: ChatEndReason;
56
+
57
+ participants: { [key: symbol]: ChatParticipantModel };
58
+ messages: ChatMessageModelBase[];
59
+ isAgentTyping: boolean;
60
+ isFilePickerEnabled: boolean;
61
+ file?: FileDescriptor;
62
+ error?: ChatUiStateError;
63
+
64
+ get agent(): ChatParticipantModel;
65
+ get user(): ChatParticipantModel;
66
+
67
+ on<T>(event: string, listener: ChatUiEventListener<T>): void;
68
+ off<T>(event: string, listener: ChatUiEventListener<T>): void;
69
+
70
+ setCustomizationContext(customizationContext?: T): void;
71
+ setIncomingSound(soundUrl: string): void;
72
+ setTimer(timer?: ChatTimer): void;
73
+ setFile(file: FileDescriptor | undefined): void;
74
+ setFilePickerEnabled(isEnabled: boolean): void;
75
+ setAgentTyping(typing: boolean): void;
76
+ setMessageState(message: ChatMessageModelBase, state: ChatMessageState, data?: any): void;
77
+ setMessages(messages: ChatMessageModelBase[]): void;
78
+ setAgentName(name: string): void;
79
+ setAgentIcon(icon: ChatParticipantIcon): void;
80
+ setError(errorTitle: string, errorMessage: string, isRecoverableError?: boolean): void;
81
+ setStatus(status: ChatRunState): void;
82
+ resetError(runState: ChatRunState): void;
83
+
84
+ triggerScroll(): void;
85
+ configure(configuration?: ChatConfiguration): void;
86
+ run(configuration?: ChatConfiguration, data?: unknown): Promise<void>;
87
+ recover(): Promise<void>;
88
+ destroy(): Promise<void>;
89
+ chasitorTyping(isTyping: boolean): Promise<void>;
90
+ restartTimers(): Promise<void>;
91
+ sendMessageText(messageText: string): Promise<void>;
92
+ sendMessageRetry(message: ChatMessageModelBase): Promise<void>;
93
+ addMessage(isAgent: boolean, message: string, data?: any): void;
94
+ addMessageWelcome(message: string): void;
95
+ }
96
+ export const CHAT_UI_STORE_TOKEN = symbolToken<IChatUiStore>('CHAT_UI_STORE_TOKEN');
97
+
98
+ @injectable()
99
+ export class ChatUiStore<T extends ChatCustomizations> implements IChatUiStore<T> {
100
+ @observable isAgentTyping = false;
101
+
102
+ @observable status = ChatRunState.Offline;
103
+
104
+ @observable isFilePickerEnabled = false;
105
+
106
+ file?: FileDescriptor;
107
+
108
+ @observable error?: ChatUiStateError;
109
+
110
+ @observable endReason?: ChatEndReason;
111
+
112
+ @observable messages: ChatMessageModelBase[] = [];
113
+
114
+ @observable currentFileMessage?: ChatMessageModelFile;
115
+
116
+ @observable scrollCounter = 1;
117
+
118
+ @observable timer?: ChatTimer;
119
+
120
+ @observable participants: { [key: symbol]: ChatParticipantModel } = {
121
+ [symbolAgent]: {
122
+ isAgent: true,
123
+ name: 'Agent',
124
+ icon: ChatParticipantIcon.Initials,
125
+ },
126
+ [symbolUser]: {
127
+ isAgent: false,
128
+ name: 'User',
129
+ icon: ChatParticipantIcon.Initials,
130
+ },
131
+ };
132
+
133
+ @computed
134
+ get agent() {
135
+ return this.participants[symbolAgent];
136
+ }
137
+
138
+ @computed
139
+ get user() {
140
+ return this.participants[symbolUser];
141
+ }
142
+
143
+ @computed
144
+ get isStarted() {
145
+ return this.status === ChatRunState.Started;
146
+ }
147
+
148
+ @computed
149
+ get isError() {
150
+ return this.status === ChatRunState.Error;
151
+ }
152
+
153
+ @computed
154
+ get isEnded() {
155
+ return this.status === ChatRunState.Ended;
156
+ }
157
+
158
+ @computed
159
+ get isStarting() {
160
+ return this.status === ChatRunState.Initializing;
161
+ }
162
+
163
+ @computed
164
+ get customizations() {
165
+ return this.customizationContext;
166
+ }
167
+
168
+ @observable
169
+ protected customizationContext: T = {} as T;
170
+ private eventEmitter = new EventEmitter();
171
+ private incomingMessageSound?: HTMLAudioElement;
172
+ private incomingMessageSoundPromise?: Promise<void>;
173
+
174
+ constructor() {
175
+ makeObservable(this);
176
+ }
177
+
178
+ on = <T>(event: string, listener: ChatUiEventListener<T>) => {
179
+ this.eventEmitter.on(event, listener);
180
+ };
181
+
182
+ off = <T>(event: string, listener: ChatUiEventListener<T>) => {
183
+ this.eventEmitter.off(event, listener);
184
+ };
185
+
186
+ @action
187
+ setCustomizationContext(customizationContext?: T) {
188
+ this.customizationContext = customizationContext ?? ({} as T);
189
+ this.triggerScroll();
190
+ }
191
+
192
+ setIncomingSound(soundUrl: string) {
193
+ if (this.incomingMessageSound) {
194
+ return;
195
+ }
196
+ this.incomingMessageSound = new Audio(soundUrl);
197
+ }
198
+
199
+ setFile(file?: FileDescriptor) {
200
+ this.file = file;
201
+ }
202
+
203
+ @action
204
+ setFilePickerEnabled(isEnabled: boolean) {
205
+ this.isFilePickerEnabled = isEnabled;
206
+ this.setFile();
207
+ this.triggerScroll();
208
+ }
209
+
210
+ @action
211
+ setTimer(timer?: ChatTimer) {
212
+ this.timer = timer;
213
+ }
214
+
215
+ @action
216
+ resetError(runState: ChatRunState) {
217
+ this.error = undefined;
218
+ this.setStatus(runState);
219
+ this.triggerScroll();
220
+ }
221
+
222
+ @action
223
+ setError(errorTitle: string, errorMessage: string, isRecoverableError = false) {
224
+ this.error = {
225
+ message: errorMessage,
226
+ isRecoverable: isRecoverableError,
227
+ title: errorTitle,
228
+ };
229
+ this.setStatus(ChatRunState.Error);
230
+ this.triggerScroll();
231
+ }
232
+
233
+ @action
234
+ setStatus(status: ChatRunState) {
235
+ this.status = status;
236
+ }
237
+
238
+ @action
239
+ setMessages(messages: ChatMessageModelBase[]) {
240
+ this.messages = messages;
241
+ this.triggerScroll();
242
+ }
243
+
244
+ @action
245
+ setAgentName(name: string) {
246
+ this.agent.name = name;
247
+ }
248
+
249
+ @action
250
+ setAgentIcon(icon: ChatParticipantIcon) {
251
+ this.agent.icon = icon;
252
+ }
253
+
254
+ @action
255
+ setAgentTyping(isAgentTyping: boolean) {
256
+ this.isAgentTyping = isAgentTyping;
257
+ this.triggerScroll();
258
+ }
259
+
260
+ @action
261
+ setEndReason(reason?: ChatEndReason) {
262
+ this.endReason = reason;
263
+ this.triggerScroll();
264
+ }
265
+
266
+ @action
267
+ setEndedChatState(endReason: ChatEndReason) {
268
+ this.setEndReason(endReason);
269
+ this.setStatus(ChatRunState.Ended);
270
+ this.setAgentTyping(false);
271
+ this.resetFile();
272
+ this.triggerScroll();
273
+ }
274
+
275
+ @action
276
+ configure(configuration?: ChatConfiguration) {
277
+ if (!configuration) {
278
+ return;
279
+ }
280
+ if (configuration.agentName) {
281
+ this.setAgentName(configuration.agentName);
282
+ }
283
+ if (configuration.agentIcon) {
284
+ this.setAgentIcon(configuration.agentIcon);
285
+ }
286
+ }
287
+
288
+ async run(configuration?: ChatConfiguration, data?: unknown) {
289
+ this.configure(configuration);
290
+ if (!this.eventEmitter.listeners(ChatUiEvent.eventRun).length) {
291
+ throw new Error("No listeners for ChatUiEvent.eventRun ('eventRun')");
292
+ }
293
+ await this.emitAsync(ChatUiEvent.eventRun, data);
294
+ }
295
+
296
+ async recover() {
297
+ await this.emitAsync(ChatUiEvent.eventRecover);
298
+ }
299
+
300
+ async destroy() {
301
+ this.reset();
302
+ await this.emitAsync(ChatUiEvent.eventDestroy);
303
+ }
304
+
305
+ restart() {
306
+ this.reset();
307
+ this.eventEmitter.emit('restart');
308
+ }
309
+
310
+ cancelChat() {
311
+ this.eventEmitter.emit('cancelChat');
312
+ }
313
+
314
+ endChat() {
315
+ this.eventEmitter.emit('endChat');
316
+ }
317
+
318
+ async chasitorTyping(isTyping: boolean) {
319
+ await this.emitAsync(
320
+ ChatUiEvent.eventChasitorTyping,
321
+ isTyping ? 'chasitorTyping' : 'chasitorNotTyping'
322
+ );
323
+ }
324
+
325
+ @action
326
+ async sendMessageText(message: string) {
327
+ await this.restartTimers();
328
+ if (this.isFilePickerEnabled && this.file) {
329
+ // Send text message with file
330
+ this.isFilePickerEnabled = false;
331
+ this.currentFileMessage = this.addMessageFile(this.file);
332
+ let messageModel: ChatMessageModelText | undefined;
333
+ if (message.trim() !== '') {
334
+ messageModel = this.addMessage(false, message);
335
+ }
336
+ await this.emitAsync(
337
+ ChatUiEvent.eventMessageSendFile,
338
+ this.currentFileMessage,
339
+ this.file,
340
+ messageModel
341
+ );
342
+ } else {
343
+ // Send plain text message
344
+ await this.sendMessageTextInternal(message);
345
+ }
346
+ }
347
+
348
+ @action
349
+ async sendMessageRetry(messageModel: ChatMessageModelBase) {
350
+ await this.restartTimers();
351
+ this.resetError(ChatRunState.Started);
352
+ this.setMessageState(messageModel, ChatMessageState.Delivering);
353
+ await this.emitAsync(ChatUiEvent.eventMessageSendRetry, messageModel);
354
+ }
355
+
356
+ @action
357
+ sendMessageFileRetry(messageModel: ChatMessageModelFile) {
358
+ this.resetError(ChatRunState.Started);
359
+ this.setMessageState(messageModel, ChatMessageState.Delivering);
360
+ this.eventEmitter.emit('sendMessageFileRetry', messageModel);
361
+ }
362
+
363
+ getParticipant(participantKey: symbol): ChatParticipantModel {
364
+ const participant = this.participants[participantKey];
365
+ return { ...participant };
366
+ }
367
+
368
+ @action
369
+ addMessage(isAgent: boolean, message: string, data?: any) {
370
+ return this.addMessageInternal<ChatMessageModelText>({
371
+ type: 'message',
372
+ state: ChatMessageState.Delivering,
373
+ participant: this.getParticipant(isAgent ? symbolAgent : symbolUser),
374
+ data,
375
+ message,
376
+ });
377
+ }
378
+
379
+ @action
380
+ addMessageTimeout() {
381
+ const isNewChat =
382
+ this.messages.length === 0 || this.messages.every(m => m.type === 'welcome');
383
+ if (isNewChat) {
384
+ return;
385
+ }
386
+ return this.addMessageInternal<ChatMessageModelTimeout>({
387
+ type: 'timeout',
388
+ participant: this.agent,
389
+ state: ChatMessageState.Delivered,
390
+ });
391
+ }
392
+
393
+ @action
394
+ addMessageWelcome(message: string) {
395
+ return this.addMessageInternal<ChatMessageModelWelcome>({
396
+ type: 'welcome',
397
+ state: ChatMessageState.Delivered,
398
+ participant: this.getParticipant(symbolAgent),
399
+ message,
400
+ });
401
+ }
402
+
403
+ @action
404
+ addMessageFile(file: FileDescriptor) {
405
+ return this.addMessageInternal<ChatMessageModelFile>({
406
+ fileName: file.displayName ?? 'attachment',
407
+ type: 'file',
408
+ state: ChatMessageState.Delivering,
409
+ participant: this.user,
410
+ });
411
+ }
412
+
413
+ @action
414
+ async restartTimers() {
415
+ this.removeMessageByType('timeout');
416
+ await this.emitAsync(ChatUiEvent.eventTimerRestart);
417
+ }
418
+
419
+ @action
420
+ setMessageState(message: ChatMessageModelBase, state: ChatMessageState, data?: any) {
421
+ const foundMessage = this.messages.find(m => m.id === message.id);
422
+ if (foundMessage) {
423
+ foundMessage.state = state;
424
+ if (data) {
425
+ foundMessage.data = data;
426
+ }
427
+ }
428
+ }
429
+
430
+ getLastFailedMessages() {
431
+ const result: ChatMessageModelText[] = [];
432
+ for (let i = this.messages.length - 1; i >= 0; i--) {
433
+ const message = this.messages[i];
434
+ if (message.type === 'message' && !message.participant.isAgent) {
435
+ if (message.state === ChatMessageState.Delivered) {
436
+ break;
437
+ }
438
+ result.push(message as ChatMessageModelText);
439
+ }
440
+ }
441
+ return result;
442
+ }
443
+
444
+ triggerScroll() {
445
+ setTimeout(() => {
446
+ runInAction(() => {
447
+ this.scrollCounter++;
448
+ });
449
+ }, 0);
450
+ }
451
+
452
+ @action
453
+ protected reset(resetStatus = true) {
454
+ this.setMessages([]);
455
+ if (resetStatus) {
456
+ this.status = ChatRunState.Offline;
457
+ }
458
+ this.error = undefined;
459
+ this.isAgentTyping = false;
460
+ this.endReason = undefined;
461
+ this.resetFile();
462
+ }
463
+
464
+ @action
465
+ protected resetFile() {
466
+ this.currentFileMessage = undefined;
467
+ this.file = undefined;
468
+ this.isFilePickerEnabled = false;
469
+ this.triggerScroll();
470
+ }
471
+
472
+ protected async emitAsync<T>(event: string, ...args: unknown[]) {
473
+ if (!this.eventEmitter.listenerCount(event)) {
474
+ return Promise.resolve();
475
+ }
476
+ return new Promise<T>((resolve, reject) => {
477
+ this.eventEmitter.emit(event, resolve, reject, ...args);
478
+ });
479
+ }
480
+
481
+ private async sendMessageTextInternal(message: string) {
482
+ if (message.trim() === '') {
483
+ return;
484
+ }
485
+ const messageModel = this.addMessage(false, message);
486
+ await this.emitAsync(ChatUiEvent.eventMessageSend, messageModel);
487
+ }
488
+
489
+ private addMessageInternal<T extends ChatMessageModelBase>(
490
+ message: Omit<T, 'id' | 'timestamp'>
491
+ ): T {
492
+ if (message.type === 'timeout') {
493
+ this.removeMessageByType('timeout'); // Remove previous timeout messages
494
+ } else {
495
+ this.restartTimers();
496
+ }
497
+ this.removeAskSupportButtons();
498
+ const newMessage = this.createMessage<T>(message);
499
+ this.setMessages([...this.messages, newMessage]);
500
+ if (message.participant.isAgent) {
501
+ this.beepIncoming().catch(() => {});
502
+ }
503
+ return newMessage;
504
+ }
505
+
506
+ private createMessage<T extends ChatMessageModelBase>(rest: Omit<T, 'id' | 'timestamp'>) {
507
+ return {
508
+ id: nanoid(),
509
+ timestamp: new Date(),
510
+ ...rest,
511
+ } as T;
512
+ }
513
+
514
+ private async beepIncoming() {
515
+ if (!this.incomingMessageSound) {
516
+ return;
517
+ }
518
+ try {
519
+ if (!this.incomingMessageSoundPromise) {
520
+ this.incomingMessageSoundPromise = this.incomingMessageSound.play();
521
+ }
522
+ await this.incomingMessageSoundPromise;
523
+ } finally {
524
+ this.incomingMessageSoundPromise = undefined;
525
+ }
526
+ }
527
+
528
+ private removeMessageByType(type: ChatMessageModelType) {
529
+ if (this.messages.some(m => m.type === type)) {
530
+ this.setMessages(this.messages.filter(m => m.type !== type));
531
+ }
532
+ }
533
+
534
+ private removeAskSupportButtons() {
535
+ this.removeMessageByType('askSupportButtons');
536
+ }
537
+ }
@@ -0,0 +1,10 @@
1
+ export {
2
+ CHAT_UI_STORE_TOKEN,
3
+ IChatUiStore,
4
+ ChatUiStore,
5
+ ChatUiEvent,
6
+ symbolUser,
7
+ symbolAgent,
8
+ ChatUiEventListener,
9
+ } from './chat-ui.store';
10
+ export { CHAT_UI_BACKEND_STORE_TOKEN, IChatUiBackendStore } from './chat-ui-backend.store';
@@ -0,0 +1,93 @@
1
+ export function formatChatMessageDate(date: Date) {
2
+ return date.toLocaleTimeString('en-US', {
3
+ hour: '2-digit',
4
+ minute: '2-digit',
5
+ });
6
+ }
7
+
8
+ export const getFirstName = (name: string): string => {
9
+ const parts = name.split(' ');
10
+ return parts[0];
11
+ };
12
+
13
+ export const getNameInitials = (name: string): string => {
14
+ const parts = name.split(' ');
15
+ return parts
16
+ .filter(Boolean)
17
+ .splice(0, 2)
18
+ .map(p => p[0]!.toUpperCase())
19
+ .join('');
20
+ };
21
+
22
+ export const getNameInitialsFirst = (name: string): string => {
23
+ const initials = getNameInitials(name);
24
+ return initials.length > 0 ? initials[0] : '';
25
+ };
26
+
27
+ export const extractFilenameAndExt = (fileName: string): [string, string] => {
28
+ const lastIndex = fileName.lastIndexOf('.');
29
+ if (lastIndex === -1) {
30
+ return [fileName, ''];
31
+ }
32
+ return [
33
+ fileName.substring(0, lastIndex) + '.',
34
+ fileName.substring(lastIndex + 1, fileName.length),
35
+ ];
36
+ };
37
+
38
+ export const ellipsisText = (
39
+ text: string,
40
+ lineLength: number,
41
+ linesCount: number,
42
+ lastLineLength: number = lineLength
43
+ ): { lines: string[]; isCut: boolean } => {
44
+ let lines = text.split('\n').map(l => l.trim());
45
+ let isCut = false;
46
+ let charsLeft = lineLength * linesCount - lastLineLength;
47
+ let linesLeft = linesCount;
48
+ for (let i = 0; i < lines.length; i++) {
49
+ let line = lines[i];
50
+ let linesInRow = Math.trunc(line.length / lineLength);
51
+ if (line.length % lineLength > 0) {
52
+ linesInRow++;
53
+ }
54
+ if (linesInRow === 0) {
55
+ linesInRow = 1;
56
+ }
57
+ linesLeft -= linesInRow;
58
+
59
+ line = line.slice(0, charsLeft);
60
+ lines[i] = line;
61
+ charsLeft = charsLeft - Math.max(lineLength, line.length);
62
+ if (charsLeft <= 0 || linesLeft <= 0) {
63
+ isCut = line.length >= lineLength - lastLineLength || i < lines.length - 1;
64
+ lines = lines.slice(0, i + 1);
65
+ if (line.length >= lineLength - lastLineLength) {
66
+ lines[i] = lines[i] + '...';
67
+ }
68
+ break;
69
+ }
70
+ }
71
+ return {
72
+ isCut,
73
+ lines,
74
+ };
75
+ };
76
+
77
+ export function extractContentFromHtml(html: string) {
78
+ const span = document.createElement('span');
79
+ span.innerHTML = html;
80
+
81
+ function getTextLoop(element: Node) {
82
+ const texts: string[] = [];
83
+ Array.from(element.childNodes).forEach(node => {
84
+ if (node.nodeType === Node.TEXT_NODE && node.textContent) {
85
+ texts.push(node.textContent.trim());
86
+ } else {
87
+ texts.push(...getTextLoop(node));
88
+ }
89
+ });
90
+ return texts;
91
+ }
92
+ return getTextLoop(span).join(' ');
93
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "extends": "@servicetitan/startup/tsconfig/base",
3
+ "compilerOptions": {
4
+ "composite": true,
5
+ "outDir": "dist",
6
+ "rootDir": "src",
7
+ "jsx": "react-jsx",
8
+ "moduleResolution": "bundler"
9
+ },
10
+ "include": ["src/**/*"],
11
+ "exclude": [
12
+ "node_modules",
13
+ "src/**/__tests__/**/*"
14
+ ]
15
+ }