@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,143 @@
1
+ import { Button, Form, FormTextArea, Stack } from '@servicetitan/design-system';
2
+ import { provide, useDependencies } from '@servicetitan/react-ioc';
3
+ import classNames from 'classnames';
4
+ import { observer } from 'mobx-react';
5
+ import { FC, useCallback, useEffect, useRef, useState } from 'react';
6
+ import { CHAT_UI_STORE_TOKEN } from '../../stores';
7
+ import { ChatInputStore } from '../../stores/chat-input.store';
8
+ import * as Styles from './chat-input.module.less';
9
+
10
+ const TIMEOUT_COOLDOWN_MS = 3000;
11
+
12
+ export const ChatInput: FC<{ className?: string }> = provide({
13
+ singletons: [ChatInputStore],
14
+ })(
15
+ observer(({ className }) => {
16
+ const typingTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
17
+ const [isSending, setIsSending] = useState(false);
18
+ const textareaRef = useRef<HTMLTextAreaElement>(null);
19
+ const [chatUiStore, supportChatInputStore] = useDependencies(
20
+ CHAT_UI_STORE_TOKEN,
21
+ ChatInputStore
22
+ );
23
+
24
+ const handleSendMessage = useCallback(async () => {
25
+ const validateResult = await supportChatInputStore.formState.validate();
26
+ if (validateResult.hasError) {
27
+ return;
28
+ }
29
+ const text = supportChatInputStore.formState.$.message.value;
30
+ if (!text.trim() && !chatUiStore.file) {
31
+ return;
32
+ }
33
+ supportChatInputStore.formState.$.message.onChange('');
34
+ setIsSending(true);
35
+ try {
36
+ await chatUiStore.sendMessageText(text);
37
+ } finally {
38
+ setIsSending(false);
39
+ }
40
+ setTimeout(() => {
41
+ textareaRef.current?.focus();
42
+ }, 0);
43
+ }, [chatUiStore, supportChatInputStore]);
44
+
45
+ const clearTimer = useCallback(() => {
46
+ clearTimeout(typingTimeoutRef.current ?? 0);
47
+ typingTimeoutRef.current = undefined;
48
+ }, []);
49
+
50
+ // Send typing status = false
51
+ const sendTypingStatusStop = useCallback(() => {
52
+ const isTyping = Boolean(typingTimeoutRef.current);
53
+ if (isTyping) {
54
+ clearTimer();
55
+ chatUiStore.chasitorTyping(false);
56
+ }
57
+ }, [chatUiStore, clearTimer]);
58
+
59
+ // Send typing status = true and start the typing countdown. After the countdown, send typing status = false
60
+ const sendTypingStatusStart = useCallback(() => {
61
+ if (!typingTimeoutRef.current) {
62
+ chatUiStore.chasitorTyping(true);
63
+ }
64
+ clearTimeout(typingTimeoutRef.current);
65
+ typingTimeoutRef.current = setTimeout(sendTypingStatusStop, TIMEOUT_COOLDOWN_MS);
66
+ }, [sendTypingStatusStop, chatUiStore]);
67
+
68
+ const setUserIsTyping = useCallback(
69
+ (typing: boolean) => {
70
+ if (typing) {
71
+ sendTypingStatusStart();
72
+ } else {
73
+ sendTypingStatusStop();
74
+ }
75
+ },
76
+ [sendTypingStatusStart, sendTypingStatusStop]
77
+ );
78
+
79
+ const handleTextKeyPress = useCallback(
80
+ (e: KeyboardEvent) => {
81
+ if (e.key === 'Enter' && !e.shiftKey) {
82
+ e.preventDefault();
83
+ clearTimer();
84
+ handleSendMessage().then(() => {});
85
+ return;
86
+ }
87
+ setUserIsTyping(true);
88
+ },
89
+ [setUserIsTyping, handleSendMessage, clearTimer]
90
+ );
91
+
92
+ useEffect(() => {
93
+ if (textareaRef.current) {
94
+ textareaRef.current.focus();
95
+ }
96
+ }, []);
97
+
98
+ return (
99
+ <Form className={className} onSubmit={handleSendMessage}>
100
+ <Stack direction="row" spacing="2">
101
+ <FormTextArea
102
+ ref={textareaRef}
103
+ name="question"
104
+ placeholder="Type your message"
105
+ rows={1}
106
+ maxRows={2}
107
+ autoHeight
108
+ onKeyPress={handleTextKeyPress}
109
+ value={supportChatInputStore.formState.$.message.value}
110
+ error={supportChatInputStore.formState.$.message.error}
111
+ onChange={supportChatInputStore.formState.$.message.onChangeHandler}
112
+ onFocus={() => {
113
+ setTimeout(() => {
114
+ chatUiStore.triggerScroll();
115
+ }, 0);
116
+ }}
117
+ onBlur={() => {
118
+ supportChatInputStore.formState.$.message.enableAutoValidationAndValidate();
119
+ }}
120
+ className={classNames('flex-grow-1', Styles.formTextarea)}
121
+ disabled={!chatUiStore.isStarted || isSending}
122
+ data-cy="titan-chat-input"
123
+ />
124
+ <Button
125
+ className="align-self-baseline"
126
+ iconName="send"
127
+ data-pendo="titan-chat-send"
128
+ data-cy="titan-chat-send"
129
+ type="submit"
130
+ primary
131
+ aria-label="submit"
132
+ disabled={
133
+ !chatUiStore.isStarted ||
134
+ isSending ||
135
+ supportChatInputStore.formState.hasError ||
136
+ supportChatInputStore.isEmpty
137
+ }
138
+ />
139
+ </Stack>
140
+ </Form>
141
+ );
142
+ })
143
+ );
@@ -0,0 +1,26 @@
1
+ import { observer } from 'mobx-react';
2
+ import { FC, PropsWithChildren, useMemo } from 'react';
3
+ import { IChatMessageProps } from '../../models';
4
+ import { getFirstName } from '../../utils/text-utils';
5
+ import { MessageAgent } from '../messages/message-agent';
6
+ import { MessageFooter } from '../messages/message-footer';
7
+ import { useAvatarProps } from '../messages/use-avatar-props';
8
+
9
+ export const ChatMessageTemplateAgent: FC<PropsWithChildren<IChatMessageProps>> = observer(
10
+ ({ children, message }) => {
11
+ const {
12
+ participant: { icon, name },
13
+ } = message;
14
+ const firstName = useMemo(() => getFirstName(name), [name]);
15
+ const avatarProps = useAvatarProps(name, icon);
16
+
17
+ return (
18
+ <MessageAgent
19
+ avatar={avatarProps}
20
+ messageFooter={<MessageFooter name={firstName} timestamp={message.timestamp} />}
21
+ >
22
+ {children}
23
+ </MessageAgent>
24
+ );
25
+ }
26
+ );
@@ -0,0 +1,46 @@
1
+ import { Button, Eyebrow } from '@servicetitan/design-system';
2
+ import { useDependencies } from '@servicetitan/react-ioc';
3
+ import { observer } from 'mobx-react';
4
+ import { FC, PropsWithChildren, useCallback } from 'react';
5
+ import { ChatMessageState, IChatMessageProps } from '../../models';
6
+ import { CHAT_UI_STORE_TOKEN } from '../../stores';
7
+ import { MessageFooter } from '../messages/message-footer';
8
+ import { MessageUser } from '../messages/message-user';
9
+
10
+ export const ChatMessageTemplateUser: FC<PropsWithChildren<IChatMessageProps>> = observer(
11
+ ({ children, message }) => {
12
+ const [chatUiStore] = useDependencies(CHAT_UI_STORE_TOKEN);
13
+ const isError = message.state === ChatMessageState.Failed;
14
+
15
+ const handleRetry = useCallback(
16
+ () => chatUiStore.sendMessageRetry(message),
17
+ [chatUiStore, message]
18
+ );
19
+
20
+ return (
21
+ <MessageUser
22
+ isError={isError}
23
+ messageFooter={
24
+ isError ? (
25
+ <Eyebrow className="c-red-600" data-cy="titan-chat-message-user-error">
26
+ Message not delivered. Retry
27
+ <Button
28
+ iconName="refresh"
29
+ fill="subtle"
30
+ size="xsmall"
31
+ negative
32
+ aria-label="Retry send message"
33
+ onClick={handleRetry}
34
+ data-cy="titan-chat-message-user-retry"
35
+ />
36
+ </Eyebrow>
37
+ ) : (
38
+ <MessageFooter timestamp={message.timestamp} />
39
+ )
40
+ }
41
+ >
42
+ {children}
43
+ </MessageUser>
44
+ );
45
+ }
46
+ );
@@ -0,0 +1,19 @@
1
+ import { useDependencies } from '@servicetitan/react-ioc';
2
+ import { observer } from 'mobx-react';
3
+ import { FC } from 'react';
4
+ import { CHAT_UI_STORE_TOKEN } from '../../stores';
5
+ import { MessageTyping } from '../messages/message-typing';
6
+ import { useAvatarProps } from '../messages/use-avatar-props';
7
+
8
+ export const ChatMessageTyping: FC = observer(() => {
9
+ const [chatUiStore] = useDependencies(CHAT_UI_STORE_TOKEN);
10
+ const [
11
+ {
12
+ agent: { icon, name },
13
+ },
14
+ ] = useDependencies(CHAT_UI_STORE_TOKEN);
15
+ const avatar = useAvatarProps(name, icon);
16
+ const Component = chatUiStore.customizations.messageTyping?.component ?? MessageTyping;
17
+
18
+ return <Component avatar={avatar} />;
19
+ });
@@ -0,0 +1,78 @@
1
+ import { BodyText } from '@servicetitan/design-system';
2
+ import { useDependencies } from '@servicetitan/react-ioc';
3
+ import { observer } from 'mobx-react';
4
+ import { FC, PropsWithChildren, ReactNode } from 'react';
5
+ import {
6
+ ChatMessageModelBase,
7
+ ChatMessageModelFile,
8
+ ChatMessageModelText,
9
+ ChatMessageModelWelcome,
10
+ } from '../../models/support-chat';
11
+ import { CHAT_UI_STORE_TOKEN } from '../../stores';
12
+ import { MessageContentFile } from '../message-content/message-content-file';
13
+ import { MessageContentText } from '../message-content/message-content-text';
14
+ import { ChatMessageTemplateAgent } from './chat-message-template-agent';
15
+ import { ChatMessageTemplateUser } from './chat-message-template-user';
16
+
17
+ interface IChatMessageProps {
18
+ message: ChatMessageModelBase;
19
+ }
20
+
21
+ /**
22
+ * ChatMessage component provides a default way to render chat messages in the chat UI and contains only most generic
23
+ * components and templates/customizations rendering
24
+ */
25
+ export const ChatMessage: FC<IChatMessageProps> = observer(({ message }) => {
26
+ const [chatUiStore] = useDependencies(CHAT_UI_STORE_TOKEN);
27
+ const isAgent = message.participant.isAgent;
28
+ const { messageTemplates, messages } = chatUiStore.customizations;
29
+ const messageCustomization = messages?.find(c => c.predicate(message));
30
+ const isSystem = Boolean(messageCustomization?.isSystem);
31
+
32
+ let messageContentNode: ReactNode | undefined;
33
+ if (messageCustomization) {
34
+ messageContentNode = <messageCustomization.component message={message} />;
35
+ } else {
36
+ // Default message content rendering
37
+ switch (message.type) {
38
+ case 'message':
39
+ case 'welcome':
40
+ messageContentNode = (
41
+ <MessageContentText
42
+ message={message as ChatMessageModelText | ChatMessageModelWelcome}
43
+ />
44
+ );
45
+ break;
46
+ case 'file':
47
+ messageContentNode = (
48
+ <MessageContentFile message={message as ChatMessageModelFile} />
49
+ );
50
+ break;
51
+ default: {
52
+ messageContentNode = <BodyText>Unknown message type: {message.type}</BodyText>;
53
+ }
54
+ }
55
+ }
56
+
57
+ if (!messageContentNode) {
58
+ return null;
59
+ }
60
+
61
+ if (isSystem) {
62
+ return messageContentNode;
63
+ }
64
+
65
+ let ComponentTemplate: FC<PropsWithChildren<IChatMessageProps>>;
66
+ if (isAgent) {
67
+ const template = messageTemplates?.agent;
68
+ ComponentTemplate = template?.predicate(message)
69
+ ? template.component
70
+ : ChatMessageTemplateAgent;
71
+ } else {
72
+ const template = messageTemplates?.user;
73
+ ComponentTemplate = template?.predicate(message)
74
+ ? template.component
75
+ : ChatMessageTemplateUser;
76
+ }
77
+ return <ComponentTemplate message={message}>{messageContentNode}</ComponentTemplate>;
78
+ });
@@ -0,0 +1,23 @@
1
+ import { Stack } from '@servicetitan/design-system';
2
+ import { useDependencies } from '@servicetitan/react-ioc';
3
+ import { observer } from 'mobx-react';
4
+ import { FC } from 'react';
5
+ import { CHAT_UI_STORE_TOKEN } from '../../stores';
6
+ import { ChatMessage } from './chat-message';
7
+ import { ChatMessageTyping } from './chat-message-typing';
8
+
9
+ interface IChatMessagesProps {
10
+ className?: string;
11
+ }
12
+
13
+ export const ChatMessages: FC<IChatMessagesProps> = observer(({ className }) => {
14
+ const [chatUiStore] = useDependencies(CHAT_UI_STORE_TOKEN);
15
+ return (
16
+ <Stack direction="column" spacing="2" className={className} data-cy="titan-chat-messages">
17
+ {chatUiStore.messages.map(message => (
18
+ <ChatMessage key={message.id} message={message} />
19
+ ))}
20
+ {chatUiStore.isAgentTyping && <ChatMessageTyping />}
21
+ </Stack>
22
+ );
23
+ });
@@ -0,0 +1,19 @@
1
+ import { Stack } from '@servicetitan/design-system';
2
+ import { useDependencies } from '@servicetitan/react-ioc';
3
+ import { observer } from 'mobx-react';
4
+ import { FC } from 'react';
5
+ import { CHAT_UI_STORE_TOKEN } from '../../stores';
6
+ import { ChatError } from './chat-error';
7
+ import { ChatTimer } from './chat-timer';
8
+
9
+ export interface IChatNotificationsProps {}
10
+
11
+ export const ChatNotifications: FC<IChatNotificationsProps> = observer(() => {
12
+ const [chatUiStore] = useDependencies(CHAT_UI_STORE_TOKEN);
13
+ return (
14
+ <Stack direction="column" spacing="2" data-cy="titan-chat-notifications">
15
+ {chatUiStore.timer && !chatUiStore.isError && <ChatTimer />}
16
+ {chatUiStore.isError && <ChatError />}
17
+ </Stack>
18
+ );
19
+ });
@@ -0,0 +1,5 @@
1
+ @import '@servicetitan/tokens/dist/tokens.less';
2
+
3
+ .banner {
4
+ border: 0 !important;
5
+ }
@@ -0,0 +1,3 @@
1
+ export const __esModule: true;
2
+ export const banner: string;
3
+
@@ -0,0 +1,35 @@
1
+ import { Banner, BodyText, Link } from '@servicetitan/design-system';
2
+ import { useDependencies } from '@servicetitan/react-ioc';
3
+ import { observer } from 'mobx-react';
4
+ import { FC, useCallback } from 'react';
5
+ import { CHAT_UI_STORE_TOKEN } from '../../stores';
6
+ import * as Styles from './chat-timer.module.less';
7
+
8
+ export const ChatTimer: FC = observer(() => {
9
+ const [chatUiStore] = useDependencies(CHAT_UI_STORE_TOKEN);
10
+ const secondsLeft = chatUiStore.timer?.secondsLeft ?? 0;
11
+
12
+ const restartTimers = useCallback(() => chatUiStore.restartTimers(), [chatUiStore]);
13
+
14
+ if (!chatUiStore.timer) {
15
+ return null;
16
+ }
17
+ return (
18
+ <Banner
19
+ status="info"
20
+ title="Chat inactive"
21
+ icon
22
+ className={Styles.banner}
23
+ data-cy="titan-chat-timer"
24
+ >
25
+ <BodyText el="p">
26
+ {'Are you still there? Please respond within '}
27
+ <b>{secondsLeft}s</b>
28
+ {' or this chat will time out.'}
29
+ </BodyText>
30
+ <Link onClick={restartTimers} subdued data-cy="titan-chat-timer-restart">
31
+ I'm here
32
+ </Link>
33
+ </Banner>
34
+ );
35
+ });
@@ -0,0 +1,55 @@
1
+ import { Stack } from '@servicetitan/design-system';
2
+ import { useDependencies } from '@servicetitan/react-ioc';
3
+ import { observer } from 'mobx-react';
4
+ import { FC, Fragment, useEffect, useRef } from 'react';
5
+ import { ChatCustomizations } from '../../models';
6
+ import { CHAT_UI_STORE_TOKEN } from '../../stores';
7
+ import { ChatConnecting } from './chat-connecting';
8
+ import { ChatInput } from './chat-input';
9
+ import { ChatInputFile } from './chat-input-file';
10
+ import { ChatMessages } from './chat-messages';
11
+ import { ChatNotifications } from './chat-notifications';
12
+
13
+ export interface IChatProps {
14
+ className?: string;
15
+ customizations?: ChatCustomizations;
16
+ }
17
+
18
+ export const Chat: FC<IChatProps> = observer(({ className, customizations }) => {
19
+ const scrollRef = useRef<HTMLDivElement>(null);
20
+ const [chatUiStore] = useDependencies(CHAT_UI_STORE_TOKEN);
21
+
22
+ const footerComponent = customizations?.footerComponent;
23
+
24
+ useEffect(() => {
25
+ chatUiStore.setCustomizationContext(customizations);
26
+ }, [chatUiStore, customizations]);
27
+
28
+ useEffect(() => {
29
+ setTimeout(() => {
30
+ if (scrollRef.current) {
31
+ scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
32
+ }
33
+ }, 0);
34
+ }, [chatUiStore.scrollCounter]);
35
+
36
+ return (
37
+ <Stack direction="column" className={className} data-cy="titan-chat">
38
+ {chatUiStore.isStarting ? (
39
+ <ChatConnecting className="p-3" />
40
+ ) : (
41
+ <Fragment>
42
+ <div ref={scrollRef} className="flex-grow-1 of-y-auto">
43
+ <ChatMessages className="p-3" />
44
+ </div>
45
+ <ChatNotifications />
46
+ <div>
47
+ <ChatInputFile className="m-3 m-b-0 p-3 border-radius-1 border border-style-dashed" />
48
+ <ChatInput className="p-x-3 p-y-2" />
49
+ {Boolean(footerComponent) && footerComponent}
50
+ </div>
51
+ </Fragment>
52
+ )}
53
+ </Stack>
54
+ );
55
+ });
@@ -0,0 +1,9 @@
1
+ @import '@servicetitan/tokens/core/tokens.less';
2
+
3
+ p.multiline-text:first-child {
4
+ margin-top: @spacing-0;
5
+ }
6
+
7
+ p.multiline-text:last-child {
8
+ margin-bottom: @spacing-0;
9
+ }
@@ -0,0 +1,3 @@
1
+ export const __esModule: true;
2
+ export const multilineText: string;
3
+
@@ -0,0 +1,30 @@
1
+ import { BodyText } from '@servicetitan/design-system';
2
+ import { BodyTextProps } from '@servicetitan/design-system/dist/components/BodyText/BodyText';
3
+ import classNames from 'classnames';
4
+ import { FC, Fragment, useMemo } from 'react';
5
+ import * as Styles from './multiline-text.module.less';
6
+
7
+ interface MultilineTextProps extends BodyTextProps {
8
+ text: string;
9
+ }
10
+
11
+ export const MultilineText: FC<MultilineTextProps> = ({ className, text, ...rest }) => {
12
+ const textParts = useMemo(() => text.split('\n'), [text]);
13
+ return (
14
+ <Fragment>
15
+ {textParts.map((part, index) => {
16
+ return (
17
+ <BodyText
18
+ el="p"
19
+ // eslint-disable-next-line react/no-array-index-key
20
+ key={index}
21
+ {...rest}
22
+ className={classNames(Styles.multilineText, className)}
23
+ >
24
+ {part.trim() ? part : <span>&nbsp;</span>}
25
+ </BodyText>
26
+ );
27
+ })}
28
+ </Fragment>
29
+ );
30
+ };
@@ -0,0 +1,27 @@
1
+ import { BodyText, Icon, Stack } from '@servicetitan/design-system';
2
+ import { observer } from 'mobx-react';
3
+ import { FC, Fragment } from 'react';
4
+ import { ChatMessageModelFile, ChatMessageState } from '../../models';
5
+
6
+ interface IMessageContentFileProps {
7
+ message: ChatMessageModelFile;
8
+ }
9
+
10
+ export const MessageContentFile: FC<IMessageContentFileProps> = observer(({ message }) => {
11
+ const isDelivering = message.state === ChatMessageState.Delivering;
12
+ const isDelivered = message.state === ChatMessageState.Delivered;
13
+
14
+ return (
15
+ <Fragment>
16
+ <Stack direction="row" spacing="1" alignItems="center">
17
+ <Icon name="attach_file" />
18
+ <span>{message.fileName}</span>
19
+ </Stack>
20
+ {(isDelivering || isDelivered) && (
21
+ <BodyText el="div" size="small" className="ta-right" subdued>
22
+ {isDelivered ? 'File uploaded' : 'Please wait while uploading...'}
23
+ </BodyText>
24
+ )}
25
+ </Fragment>
26
+ );
27
+ });
@@ -0,0 +1,12 @@
1
+ import { observer } from 'mobx-react';
2
+ import { FC } from 'react';
3
+ import { ChatMessageModelText, ChatMessageModelWelcome } from '../../models/support-chat';
4
+ import { MultilineText } from '../common/multiline-text';
5
+
6
+ interface IMessageContentTextProps {
7
+ message: ChatMessageModelText | ChatMessageModelWelcome;
8
+ }
9
+
10
+ export const MessageContentText: FC<IMessageContentTextProps> = observer(({ message }) => {
11
+ return <MultilineText text={message.message} />;
12
+ });