@servicetitan/titan-chat-ui-anvil2 3.1.0 → 3.1.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 (112) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/components/chat/__tests-cy__/chat-error.test.d.ts +2 -0
  3. package/dist/components/chat/__tests-cy__/chat-error.test.d.ts.map +1 -0
  4. package/dist/components/chat/__tests-cy__/chat-error.test.js +6 -0
  5. package/dist/components/chat/__tests-cy__/chat-error.test.js.map +1 -0
  6. package/dist/components/chat/__tests-cy__/chat-input-file.test.d.ts +2 -0
  7. package/dist/components/chat/__tests-cy__/chat-input-file.test.d.ts.map +1 -0
  8. package/dist/components/chat/__tests-cy__/chat-input-file.test.js +6 -0
  9. package/dist/components/chat/__tests-cy__/chat-input-file.test.js.map +1 -0
  10. package/dist/components/chat/__tests-cy__/chat-input.test.d.ts +2 -0
  11. package/dist/components/chat/__tests-cy__/chat-input.test.d.ts.map +1 -0
  12. package/dist/components/chat/__tests-cy__/chat-input.test.js +6 -0
  13. package/dist/components/chat/__tests-cy__/chat-input.test.js.map +1 -0
  14. package/dist/components/chat/__tests-cy__/chat-log.test.d.ts +2 -0
  15. package/dist/components/chat/__tests-cy__/chat-log.test.d.ts.map +1 -0
  16. package/dist/components/chat/__tests-cy__/chat-log.test.js +6 -0
  17. package/dist/components/chat/__tests-cy__/chat-log.test.js.map +1 -0
  18. package/dist/components/chat/__tests-cy__/chat-messages.test.js +2 -91
  19. package/dist/components/chat/__tests-cy__/chat-messages.test.js.map +1 -1
  20. package/dist/components/chat/__tests-cy__/chat-notifications.test.d.ts +2 -0
  21. package/dist/components/chat/__tests-cy__/chat-notifications.test.d.ts.map +1 -0
  22. package/dist/components/chat/__tests-cy__/chat-notifications.test.js +6 -0
  23. package/dist/components/chat/__tests-cy__/chat-notifications.test.js.map +1 -0
  24. package/dist/components/chat/__tests-cy__/chat-timer.test.d.ts +2 -0
  25. package/dist/components/chat/__tests-cy__/chat-timer.test.d.ts.map +1 -0
  26. package/dist/components/chat/__tests-cy__/chat-timer.test.js +6 -0
  27. package/dist/components/chat/__tests-cy__/chat-timer.test.js.map +1 -0
  28. package/dist/components/chat/__tests-cy__/chat.test.js +3 -127
  29. package/dist/components/chat/__tests-cy__/chat.test.js.map +1 -1
  30. package/dist/components/chat/chat-connecting.js +1 -1
  31. package/dist/components/chat/chat-connecting.js.map +1 -1
  32. package/dist/components/chat/chat-error.d.ts +3 -1
  33. package/dist/components/chat/chat-error.d.ts.map +1 -1
  34. package/dist/components/chat/chat-error.js +3 -4
  35. package/dist/components/chat/chat-error.js.map +1 -1
  36. package/dist/components/chat/chat-input-file.d.ts.map +1 -1
  37. package/dist/components/chat/chat-input-file.js +19 -15
  38. package/dist/components/chat/chat-input-file.js.map +1 -1
  39. package/dist/components/chat/chat-input.d.ts.map +1 -1
  40. package/dist/components/chat/chat-input.js +5 -6
  41. package/dist/components/chat/chat-input.js.map +1 -1
  42. package/dist/components/chat/chat-input.module.less +4 -0
  43. package/dist/components/chat/chat-message-template-user.d.ts.map +1 -1
  44. package/dist/components/chat/chat-message-template-user.js +2 -2
  45. package/dist/components/chat/chat-message-template-user.js.map +1 -1
  46. package/dist/components/chat/chat-timer.d.ts.map +1 -1
  47. package/dist/components/chat/chat-timer.js +2 -3
  48. package/dist/components/chat/chat-timer.js.map +1 -1
  49. package/dist/components/chat/chat.d.ts.map +1 -1
  50. package/dist/components/chat/chat.js +7 -2
  51. package/dist/components/chat/chat.js.map +1 -1
  52. package/dist/components/chat/chat.module.less +9 -0
  53. package/dist/components/message-content/__tests-cy__/message-content-file.test.d.ts +2 -0
  54. package/dist/components/message-content/__tests-cy__/message-content-file.test.d.ts.map +1 -0
  55. package/dist/components/message-content/__tests-cy__/message-content-file.test.js +6 -0
  56. package/dist/components/message-content/__tests-cy__/message-content-file.test.js.map +1 -0
  57. package/dist/components/messages/__tests-cy__/message-agent.test.js +11 -77
  58. package/dist/components/messages/__tests-cy__/message-agent.test.js.map +1 -1
  59. package/dist/components/messages/__tests-cy__/message-system.test.js +6 -15
  60. package/dist/components/messages/__tests-cy__/message-system.test.js.map +1 -1
  61. package/dist/components/messages/__tests-cy__/message-timeout.test.js +2 -21
  62. package/dist/components/messages/__tests-cy__/message-timeout.test.js.map +1 -1
  63. package/dist/components/messages/__tests-cy__/message-typing.test.js +3 -45
  64. package/dist/components/messages/__tests-cy__/message-typing.test.js.map +1 -1
  65. package/dist/components/messages/__tests-cy__/message-user.test.js +3 -23
  66. package/dist/components/messages/__tests-cy__/message-user.test.js.map +1 -1
  67. package/dist/components/messages/message-timeout.d.ts.map +1 -1
  68. package/dist/components/messages/message-timeout.js +2 -1
  69. package/dist/components/messages/message-timeout.js.map +1 -1
  70. package/dist/components/messages/message-timeout.module.less +3 -0
  71. package/dist/components/messages/message-typing.js +1 -1
  72. package/dist/components/messages/message-typing.js.map +1 -1
  73. package/dist/components/messages/message-typing.module.less +4 -0
  74. package/package.json +4 -4
  75. package/src/components/chat/__tests-cy__/chat-error.test.tsx +6 -0
  76. package/src/components/chat/__tests-cy__/chat-input-file.test.tsx +6 -0
  77. package/src/components/chat/__tests-cy__/chat-input.test.tsx +6 -0
  78. package/src/components/chat/__tests-cy__/chat-log.test.tsx +6 -0
  79. package/src/components/chat/__tests-cy__/chat-messages.test.tsx +2 -107
  80. package/src/components/chat/__tests-cy__/chat-notifications.test.tsx +6 -0
  81. package/src/components/chat/__tests-cy__/chat-timer.test.tsx +6 -0
  82. package/src/components/chat/__tests-cy__/chat.test.tsx +3 -161
  83. package/src/components/chat/chat-connecting.tsx +1 -1
  84. package/src/components/chat/chat-error.tsx +18 -21
  85. package/src/components/chat/chat-input-file.tsx +66 -31
  86. package/src/components/chat/chat-input.module.less +4 -0
  87. package/src/components/chat/chat-input.module.less.d.ts +1 -0
  88. package/src/components/chat/chat-input.tsx +31 -26
  89. package/src/components/chat/chat-message-template-user.tsx +11 -8
  90. package/src/components/chat/chat-timer.tsx +3 -9
  91. package/src/components/chat/chat.module.less +9 -0
  92. package/src/components/chat/chat.module.less.d.ts +4 -0
  93. package/src/components/chat/chat.tsx +33 -20
  94. package/src/components/message-content/__tests-cy__/message-content-file.test.tsx +6 -0
  95. package/src/components/messages/__tests-cy__/message-agent.test.tsx +15 -136
  96. package/src/components/messages/__tests-cy__/message-system.test.tsx +10 -28
  97. package/src/components/messages/__tests-cy__/message-timeout.test.tsx +2 -25
  98. package/src/components/messages/__tests-cy__/message-typing.test.tsx +3 -54
  99. package/src/components/messages/__tests-cy__/message-user.test.tsx +3 -37
  100. package/src/components/messages/message-timeout.module.less +3 -0
  101. package/src/components/messages/message-timeout.module.less.d.ts +3 -0
  102. package/src/components/messages/message-timeout.tsx +3 -2
  103. package/src/components/messages/message-typing.module.less +4 -0
  104. package/src/components/messages/message-typing.module.less.d.ts +1 -0
  105. package/src/components/messages/message-typing.tsx +1 -1
  106. package/tsconfig.tsbuildinfo +1 -1
  107. package/dist/components/chat/chat-error.module.less +0 -6
  108. package/dist/components/chat/chat-timer.module.less +0 -5
  109. package/src/components/chat/chat-error.module.less +0 -6
  110. package/src/components/chat/chat-error.module.less.d.ts +0 -3
  111. package/src/components/chat/chat-timer.module.less +0 -5
  112. package/src/components/chat/chat-timer.module.less.d.ts +0 -3
@@ -1,111 +1,6 @@
1
- import { Provider } from '@servicetitan/react-ioc';
2
- import { CHAT_UI_STORE_TOKEN, mockChatMessageModelText } from '@servicetitan/titan-chat-ui-common';
3
- import { ChatUiSelectors, CypressMocks } from '@servicetitan/titan-chatbot-ui-cypress';
4
- import { mount } from 'cypress/react';
1
+ import { runChatMessagesSharedTests } from '@servicetitan/titan-chatbot-ui-cypress';
5
2
  import { ChatMessages } from '../chat-messages';
6
3
 
7
4
  describe('[ChatMessages]', () => {
8
- let storeMock: CypressMocks.ChatUiStoreMock;
9
-
10
- beforeEach(() => {
11
- cy.viewport(780, 600);
12
- cy.clock(new Date('2023-10-01T10:10:00Z').getTime());
13
- storeMock = new CypressMocks.ChatUiStoreMock();
14
- });
15
-
16
- const render = () => {
17
- return mount(
18
- <Provider
19
- singletons={[
20
- {
21
- provide: CHAT_UI_STORE_TOKEN,
22
- useValue: storeMock,
23
- },
24
- ]}
25
- >
26
- <ChatMessages messages={storeMock.messages} />
27
- </Provider>
28
- );
29
- };
30
-
31
- it('should render default chat', () => {
32
- render();
33
-
34
- ChatUiSelectors.chatMessages.should('be.visible');
35
- ChatUiSelectors.chatMessage.should('have.length', 2);
36
- });
37
-
38
- it('should render several consecutive agent messages without extra avatars', () => {
39
- storeMock.messages = [
40
- mockChatMessageModelText(true, {
41
- id: 'id1',
42
- timestamp: new Date('2023-01-01T10:10:00Z'),
43
- message: 'Hello, this is the first message',
44
- }),
45
- mockChatMessageModelText(true, {
46
- id: 'id2',
47
- message: 'Hello, this is the second message. '.repeat(5).trim(),
48
- timestamp: new Date('2023-01-01T10:10:59.999Z'),
49
- }),
50
- mockChatMessageModelText(true, {
51
- id: 'id3',
52
- message: 'Hello, this is the third message',
53
- timestamp: new Date('2023-01-01T10:11:00Z'),
54
- }),
55
- mockChatMessageModelText(true, {
56
- id: 'id4',
57
- message: 'Hello, this is the forth message',
58
- timestamp: new Date('2023-01-01T10:11:01Z'),
59
- }),
60
- mockChatMessageModelText(false, {
61
- id: 'id11',
62
- timestamp: new Date('2023-01-01T11:10:00Z'),
63
- message: 'Hello, this is the first message',
64
- }),
65
- mockChatMessageModelText(false, {
66
- id: 'id22',
67
- message: 'Hello, this is the second message',
68
- timestamp: new Date('2023-01-01T11:11:00Z'),
69
- }),
70
- mockChatMessageModelText(false, {
71
- id: 'id33',
72
- message: 'Hello, this is the third message',
73
- timestamp: new Date('2023-01-01T11:12:00Z'),
74
- }),
75
- mockChatMessageModelText(false, {
76
- id: 'id44',
77
- message: 'Hello, this is the forth message',
78
- timestamp: new Date('2023-01-01T11:12:01Z'),
79
- }),
80
- ];
81
- render();
82
-
83
- const getTimestamp = (i: number) =>
84
- ChatUiSelectors.chatMessage
85
- .eq(i)
86
- .find(`[data-cy="${ChatUiSelectors.cy.chatMessageFooter}"]`);
87
-
88
- ChatUiSelectors.chatMessageAgent.should('have.length', 4);
89
- ChatUiSelectors.chatMessageUser.should('have.length', 4);
90
-
91
- // Agent avatar should be visible only for the first message in group
92
- const getAvatar = (i: number) =>
93
- ChatUiSelectors.chatMessageAgent
94
- .find(`[data-cy="${ChatUiSelectors.cy.chatAvatar}"]`)
95
- .eq(i);
96
- getAvatar(0).should('be.visible');
97
- getAvatar(1).should('not.exist');
98
- getAvatar(2).should('not.exist');
99
- getAvatar(3).should('not.exist');
100
-
101
- // Footer with date should be visible only for the different formatted timestamps
102
- getTimestamp(0).should('not.exist');
103
- getTimestamp(1).should('be.visible').should('contain.text', 'agent • 10:10 AM');
104
- getTimestamp(2).should('not.exist');
105
- getTimestamp(3).should('be.visible').should('contain.text', 'agent • 10:11 AM');
106
- getTimestamp(4).should('be.visible').should('contain.text', '11:10 AM');
107
- getTimestamp(5).should('be.visible').should('contain.text', '11:11 AM');
108
- getTimestamp(6).should('not.exist');
109
- getTimestamp(7).should('be.visible').should('contain.text', '11:12 AM');
110
- });
5
+ runChatMessagesSharedTests(ChatMessages);
111
6
  });
@@ -0,0 +1,6 @@
1
+ import { runChatNotificationsTests } from '@servicetitan/titan-chatbot-ui-cypress';
2
+ import { ChatNotifications } from '../chat-notifications';
3
+
4
+ describe('ChatNotifications', () => {
5
+ runChatNotificationsTests(ChatNotifications);
6
+ });
@@ -0,0 +1,6 @@
1
+ import { runChatTimerTests } from '@servicetitan/titan-chatbot-ui-cypress';
2
+ import { ChatTimer } from '../chat-timer';
3
+
4
+ describe('ChatTimer', () => {
5
+ runChatTimerTests(ChatTimer);
6
+ });
@@ -1,164 +1,6 @@
1
- import { AnvilProvider } from '@servicetitan/anvil2';
2
- import { Container, provide, useDependencies } from '@servicetitan/react-ioc';
3
- import {
4
- CHAT_UI_BACKEND_STORE_TOKEN,
5
- CHAT_UI_STORE_TOKEN,
6
- ChatParticipantIcon,
7
- ChatUiBackendEchoStore,
8
- ChatUiStore,
9
- IChatUiBackendStore,
10
- IChatUiStore,
11
- } from '@servicetitan/titan-chat-ui-common';
12
- import { ChatUiSelectors } from '@servicetitan/titan-chatbot-ui-cypress';
13
- import { mount as cyMount } from 'cypress/react';
14
- import { ReactNode, useEffect } from 'react';
1
+ import { runChatSharedTests } from '@servicetitan/titan-chatbot-ui-cypress';
15
2
  import { Chat } from '../chat';
16
3
 
17
- const mount = (children: ReactNode) => {
18
- return cyMount(<AnvilProvider>{children}</AnvilProvider>);
19
- };
20
-
21
- const initContainer = () => {
22
- const rootContainer = new Container();
23
- const container = new Container();
24
- container.parent = rootContainer;
25
- container.bind<IChatUiStore>(CHAT_UI_STORE_TOKEN).to(ChatUiStore).inSingletonScope();
26
- container
27
- .bind<IChatUiBackendStore>(CHAT_UI_BACKEND_STORE_TOKEN)
28
- .to(ChatUiBackendEchoStore)
29
- .inSingletonScope();
30
- return container;
31
- };
32
-
33
- describe('[Chat]', () => {
34
- let container: Container;
35
- let chatUiStore: IChatUiStore;
36
- let chatUiBackendStore: ChatUiBackendEchoStore;
37
-
38
- beforeEach(() => {
39
- container = initContainer();
40
- chatUiStore = container.get<IChatUiStore>(CHAT_UI_STORE_TOKEN);
41
- chatUiBackendStore = container.get<ChatUiBackendEchoStore>(CHAT_UI_BACKEND_STORE_TOKEN);
42
- cy.viewport(780, 800);
43
- cy.clock(Date.parse('2023-10-01T00:00:00Z'));
44
- });
45
-
46
- const render = () => {
47
- const ChatWrapper = provide({
48
- singletons: [
49
- {
50
- provide: CHAT_UI_STORE_TOKEN,
51
- useValue: chatUiStore,
52
- },
53
- {
54
- provide: CHAT_UI_BACKEND_STORE_TOKEN,
55
- useValue: chatUiBackendStore,
56
- },
57
- ],
58
- })(() => {
59
- const [chatUiStore, chatUiBackendStore] = useDependencies(
60
- CHAT_UI_STORE_TOKEN,
61
- CHAT_UI_BACKEND_STORE_TOKEN
62
- );
63
- useEffect(() => {
64
- const init = async () => {
65
- chatUiBackendStore.subscribe();
66
- await chatUiStore.run({
67
- agentName: 'EchoBot',
68
- agentIcon: ChatParticipantIcon.Bot,
69
- });
70
- };
71
- init().then(() => {});
72
- return () => chatUiBackendStore.unsubscribe();
73
- }, [chatUiStore, chatUiBackendStore]);
74
- return <Chat className="h-100vh max-h-100vh" />;
75
- });
76
-
77
- cy.spy(chatUiStore, 'run').as('runSpy');
78
- mount(<ChatWrapper />);
79
-
80
- ChatUiSelectors.chatConnecting.should('be.visible');
81
- cy.tick(1000);
82
- return cy.wrap(
83
- new Promise(resolve => {
84
- cy.get('@runSpy')
85
- .should('have.been.calledOnce')
86
- .then((invocation: any) => {
87
- const initPromise = invocation.firstCall.returnValue as ReturnType<
88
- IChatUiStore['run']
89
- >;
90
- initPromise.then(resolve);
91
- });
92
- })
93
- );
94
- };
95
-
96
- const ask = (message: string) => {
97
- ChatUiSelectors.chatInput.type(`${message}{enter}`);
98
- };
99
-
100
- it('should render default chat', () => {
101
- render().then(() => {
102
- ChatUiSelectors.chatMessages.should('be.visible');
103
- ChatUiSelectors.chatMessageAgent.should('have.length', 1).should('be.visible');
104
- ChatUiSelectors.chatMessageContentAgent
105
- .should('be.visible')
106
- .should(
107
- 'contain.text',
108
- "Hello! I'm generic echo bot. I can echo your messages. Try it out!"
109
- );
110
- ChatUiSelectors.chatNotifications.should('exist');
111
- ChatUiSelectors.chatSend.should('be.visible').should('be.disabled');
112
- ChatUiSelectors.chatInput.should('be.visible').should('not.be.disabled');
113
-
114
- ChatUiSelectors.chatInput.type('Hello');
115
- ChatUiSelectors.chatSend.click();
116
-
117
- ChatUiSelectors.chatMessageUser
118
- .should('be.visible')
119
- .should('have.length', 1)
120
- .should('contain.text', 'Hello');
121
- ChatUiSelectors.chatMessageTyping.should('be.visible');
122
- ChatUiSelectors.chatMessageAgent.should('have.length', 1);
123
-
124
- cy.tick(1000);
125
-
126
- ChatUiSelectors.chatMessageTyping.should('not.exist');
127
- ChatUiSelectors.chatMessageAgent.should('have.length', 2);
128
- });
129
- });
130
-
131
- it('should render chat error message', () => {
132
- render().then(() => {
133
- chatUiStore.setTimer({ secondsTotal: 100, secondsLeft: 10 });
134
- chatUiStore.setError('error message', {
135
- title: 'Custom Error',
136
- recoverStrategy: {
137
- recoverButtonTitle: 'Recover button title',
138
- },
139
- });
140
-
141
- ChatUiSelectors.chatTimer.should('not.exist');
142
- ChatUiSelectors.chatError.should('be.visible');
143
- });
144
- });
145
-
146
- it('should handle send message error and retry', () => {
147
- render().then(() => {
148
- ask('[Error]Custom error message');
149
- cy.tick(1000);
150
-
151
- // Check error message
152
- ChatUiSelectors.chatMessageError
153
- .should('be.visible')
154
- .should('contain.text', 'Message not delivered. Retry');
155
- ChatUiSelectors.chatError
156
- .should('be.visible')
157
- .should('contain.text', ['Custom Error Title', 'Custom error message'].join(''));
158
-
159
- // Retry message
160
- ChatUiSelectors.chatMessageErrorRetry.should('be.visible').click();
161
- cy.tick(1000);
162
- });
163
- });
4
+ describe('Chat', () => {
5
+ runChatSharedTests(Chat);
164
6
  });
@@ -16,7 +16,7 @@ export const ChatConnecting: FC<IChatConnectingProps> = ({ className }) => {
16
16
  >
17
17
  <Flex direction="row" gap="4" alignItems="center">
18
18
  <Spinner />
19
- <Text className="c-neutral-100">Starting...</Text>
19
+ <Text className="c-subdued">Starting...</Text>
20
20
  </Flex>
21
21
  </Flex>
22
22
  );
@@ -1,12 +1,11 @@
1
- import { Announcement, Button } from '@servicetitan/anvil2';
1
+ import { Alert, Button, Flex } from '@servicetitan/anvil2';
2
2
  import { useDependencies } from '@servicetitan/react-ioc';
3
3
  import { CHAT_UI_STORE_TOKEN } from '@servicetitan/titan-chat-ui-common';
4
4
  import { observer } from 'mobx-react';
5
5
  import { FC, useCallback } from 'react';
6
6
  import { MultilineText } from '../common/multiline-text';
7
- import * as Styles from './chat-error.module.less';
8
7
 
9
- export const ChatError: FC = observer(() => {
8
+ export const ChatError: FC<{ className?: string }> = observer(({ className }) => {
10
9
  const [chatUiStore] = useDependencies(CHAT_UI_STORE_TOKEN);
11
10
  const { error } = chatUiStore;
12
11
 
@@ -18,23 +17,21 @@ export const ChatError: FC = observer(() => {
18
17
  return null;
19
18
  }
20
19
  return (
21
- <Announcement
22
- status="danger"
23
- title={error.title}
24
- className={Styles.banner}
25
- data-cy="titan-chat-error"
26
- >
27
- <MultilineText text={error.message} data-cy="titan-chat-error-text" />
28
- {error.recoverStrategy && (
29
- <Button
30
- className="m-t-2 bg-white-i"
31
- size="small"
32
- onClick={handleReconnect}
33
- data-cy="titan-chat-error-recover"
34
- >
35
- {error.recoverStrategy.recoverButtonTitle}
36
- </Button>
37
- )}
38
- </Announcement>
20
+ <Alert status="danger" title={error.title} className={className} data-cy="titan-chat-error">
21
+ <Flex direction="column" gap="4">
22
+ <MultilineText text={error.message} data-cy="titan-chat-error-text" />
23
+ {error.recoverStrategy && (
24
+ <Button
25
+ type="button"
26
+ appearance="danger"
27
+ size="small"
28
+ onClick={handleReconnect}
29
+ data-cy="titan-chat-error-recover"
30
+ >
31
+ {error.recoverStrategy.recoverButtonTitle}
32
+ </Button>
33
+ )}
34
+ </Flex>
35
+ </Alert>
39
36
  );
40
37
  });
@@ -1,20 +1,28 @@
1
- /* eslint-disable @typescript-eslint/no-unused-vars */
2
- import { Flex, Text } from '@servicetitan/anvil2';
1
+ import { Button, Card, Flex, Icon, Text } from '@servicetitan/anvil2';
2
+ import IconAttachment from '@servicetitan/anvil2/assets/icons/material/round/attach_file.svg';
3
+ import IconDelete from '@servicetitan/anvil2/assets/icons/material/round/delete.svg';
4
+ import IconFile from '@servicetitan/anvil2/assets/icons/material/round/insert_drive_file.svg';
5
+ import IconEdit from '@servicetitan/anvil2/assets/icons/material/round/refresh.svg';
3
6
  import { FileDescriptor } from '@servicetitan/form';
4
7
  import { useDependencies } from '@servicetitan/react-ioc';
5
8
  import { CHAT_UI_STORE_TOKEN } from '@servicetitan/titan-chat-ui-common';
6
9
  import { observer } from 'mobx-react';
7
- import { FC, useCallback, useEffect, useState } from 'react';
10
+ import { ChangeEvent, FC, Fragment, useCallback, useEffect, useRef, useState } from 'react';
8
11
 
9
12
  export const ChatInputFile: FC<{ className?: string }> = observer(({ className }) => {
13
+ const fileInputRef = useRef<HTMLInputElement>(null);
10
14
  const [chatUiStore] = useDependencies(CHAT_UI_STORE_TOKEN);
11
15
  const [fileDescriptor, setFileDescriptor] = useState<FileDescriptor | undefined>(undefined);
12
16
 
13
17
  const handleSelected = useCallback(
14
- (newAttachments: FileList) => {
18
+ (event: ChangeEvent<HTMLInputElement>) => {
19
+ const files = event.target.files;
20
+ if (!files || files.length === 0) {
21
+ return;
22
+ }
15
23
  const fileDescriptor: FileDescriptor = {
16
- file: newAttachments[0],
17
- displayName: newAttachments[0].name,
24
+ file: files[0],
25
+ displayName: files[0].name,
18
26
  };
19
27
  setFileDescriptor(fileDescriptor);
20
28
  chatUiStore.setFile(fileDescriptor);
@@ -22,17 +30,9 @@ export const ChatInputFile: FC<{ className?: string }> = observer(({ className }
22
30
  [chatUiStore]
23
31
  );
24
32
 
25
- const handleReplace = useCallback(
26
- ({ newFile }: { file: FileDescriptor; newFile: File }) => {
27
- const fileDescriptor: FileDescriptor = {
28
- file: newFile,
29
- displayName: newFile.name,
30
- };
31
- setFileDescriptor(fileDescriptor);
32
- chatUiStore.setFile(fileDescriptor);
33
- },
34
- [chatUiStore]
35
- );
33
+ const handleUpload = () => {
34
+ fileInputRef.current?.click();
35
+ };
36
36
 
37
37
  const handleDelete = useCallback(() => {
38
38
  setFileDescriptor(undefined);
@@ -51,20 +51,55 @@ export const ChatInputFile: FC<{ className?: string }> = observer(({ className }
51
51
  return (
52
52
  <Flex className={className} gap="6" direction="column">
53
53
  <Text variant="eyebrow">Upload file</Text>
54
- {/* <FilePicker */}
55
- {/* name="attachments" */}
56
- {/* buttonProps={{ */}
57
- {/* color: 'primary', */}
58
- {/* fill: 'outline', */}
59
- {/* }} */}
60
- {/* typesNote="e.g. Screenshot of issue" */}
61
- {/* value={fileDescriptor} */}
62
- {/* onSelected={handleSelected} */}
63
- {/* onDelete={handleDelete} */}
64
- {/* onReplace={handleReplace} */}
65
- {/* data-cy="titan-chat-upload-file" */}
66
- {/* /> */}
67
- FILE PICKER PLACEHOLDER
54
+ <input
55
+ type="file"
56
+ ref={fileInputRef}
57
+ onChange={handleSelected}
58
+ style={{ display: 'none' }} // Hide the native input
59
+ accept="*/*"
60
+ multiple={false}
61
+ />
62
+ <Flex direction="column" gap="2" data-cy="titan-chat-upload-file">
63
+ {fileDescriptor ? (
64
+ <Card padding="small">
65
+ <Flex style={{ width: '100%' }} direction="row" alignItems="center" gap="2">
66
+ <Icon svg={IconFile} className="m-inline-start-4" />
67
+ <Text variant="body" flexGrow={1} data-cy="titan-chat-upload-file-name">
68
+ {fileDescriptor.displayName}
69
+ </Text>
70
+ <Button
71
+ icon={IconEdit}
72
+ aria-label="Replace file"
73
+ appearance="ghost"
74
+ onClick={handleUpload}
75
+ data-cy="titan-chat-upload-file-edit"
76
+ />
77
+ <Button
78
+ icon={IconDelete}
79
+ aria-label="Delete file"
80
+ appearance="ghost"
81
+ onClick={handleDelete}
82
+ data-cy="titan-chat-upload-file-delete"
83
+ />
84
+ </Flex>
85
+ </Card>
86
+ ) : (
87
+ <Fragment>
88
+ <Button
89
+ type="button"
90
+ appearance="secondary"
91
+ icon={IconAttachment}
92
+ onClick={handleUpload}
93
+ data-cy="titan-chat-upload-file-btn"
94
+ >
95
+ Upload File
96
+ </Button>
97
+ <Text variant="body" size="small" subdued>
98
+ e.g. Screenshot of issue
99
+ </Text>
100
+ </Fragment>
101
+ )}
102
+ </Flex>
68
103
  </Flex>
69
104
  );
70
105
  });
@@ -1,5 +1,9 @@
1
1
  @import '@servicetitan/tokens/dist/tokens.less';
2
2
 
3
+ .formContainer {
4
+ width: 100%;
5
+ }
6
+
3
7
  .form-textarea {
4
8
  margin-bottom: @spacing-0 !important;
5
9
 
@@ -1,3 +1,4 @@
1
1
  export const __esModule: true;
2
+ export const formContainer: string;
2
3
  export const formTextarea: string;
3
4
 
@@ -1,10 +1,10 @@
1
- import { Button, Textarea } from '@servicetitan/anvil2';
1
+ import { Button, Flex, Textarea } from '@servicetitan/anvil2';
2
2
  import IconSend from '@servicetitan/anvil2/assets/icons/material/round/send.svg';
3
3
  import { provide, useDependencies } from '@servicetitan/react-ioc';
4
4
  import { CHAT_UI_STORE_TOKEN } from '@servicetitan/titan-chat-ui-common';
5
5
  import classNames from 'classnames';
6
6
  import { observer } from 'mobx-react';
7
- import { FC, KeyboardEvent, useCallback, useEffect, useRef, useState } from 'react';
7
+ import { FC, FormEvent, KeyboardEvent, useCallback, useEffect, useRef, useState } from 'react';
8
8
  import { ChatInputStore } from '../../stores/chat-input.store';
9
9
  import * as Styles from './chat-input.module.less';
10
10
 
@@ -22,26 +22,30 @@ export const ChatInput: FC<{ className?: string }> = provide({
22
22
  ChatInputStore
23
23
  );
24
24
 
25
- const handleSendMessage = useCallback(async () => {
26
- const validateResult = await supportChatInputStore.formState.validate();
27
- if (validateResult.hasError) {
28
- return;
29
- }
30
- const text = supportChatInputStore.formState.$.message.value;
31
- if (!text.trim() && !chatUiStore.file) {
32
- return;
33
- }
34
- supportChatInputStore.formState.$.message.onChange('');
35
- setIsSending(true);
36
- try {
37
- await chatUiStore.sendMessageText(text);
38
- } finally {
39
- setIsSending(false);
40
- }
41
- setTimeout(() => {
42
- textareaRef.current?.focus();
43
- }, 0);
44
- }, [chatUiStore, supportChatInputStore]);
25
+ const handleSendMessage = useCallback(
26
+ async (event?: FormEvent) => {
27
+ event?.preventDefault();
28
+ const validateResult = await supportChatInputStore.formState.validate();
29
+ if (validateResult.hasError) {
30
+ return;
31
+ }
32
+ const text = supportChatInputStore.formState.$.message.value;
33
+ if (!text.trim() && !chatUiStore.file) {
34
+ return;
35
+ }
36
+ supportChatInputStore.formState.$.message.onChange('');
37
+ setIsSending(true);
38
+ try {
39
+ await chatUiStore.sendMessageText(text);
40
+ } finally {
41
+ setIsSending(false);
42
+ }
43
+ setTimeout(() => {
44
+ textareaRef.current?.focus();
45
+ }, 0);
46
+ },
47
+ [chatUiStore, supportChatInputStore]
48
+ );
45
49
 
46
50
  const clearTimer = useCallback(() => {
47
51
  clearTimeout(typingTimeoutRef.current ?? 0);
@@ -98,14 +102,15 @@ export const ChatInput: FC<{ className?: string }> = provide({
98
102
 
99
103
  return (
100
104
  <form className={classNames(className)} onSubmit={handleSendMessage}>
101
- <div className={classNames('d-f flex-row gap-2')}>
105
+ <Flex direction="row" gap={4} className={Styles.formContainer}>
102
106
  <Textarea
103
107
  ref={textareaRef}
104
108
  name="question"
105
109
  placeholder="Type your message"
106
110
  rows={1}
107
- // maxRows={2}
111
+ maxRows={2}
108
112
  autoHeight
113
+ flexGrow={1}
109
114
  onKeyDown={handleTextKeyPress}
110
115
  value={supportChatInputStore.formState.$.message.value}
111
116
  error={supportChatInputStore.formState.$.message.error}
@@ -123,7 +128,7 @@ export const ChatInput: FC<{ className?: string }> = provide({
123
128
  data-cy="titan-chat-input"
124
129
  />
125
130
  <Button
126
- className="align-self-baseline"
131
+ alignSelf="baseline"
127
132
  icon={IconSend}
128
133
  data-pendo="titan-chat-send"
129
134
  data-cy="titan-chat-send"
@@ -137,7 +142,7 @@ export const ChatInput: FC<{ className?: string }> = provide({
137
142
  supportChatInputStore.isEmpty
138
143
  }
139
144
  />
140
- </div>
145
+ </Flex>
141
146
  </form>
142
147
  );
143
148
  })
@@ -1,4 +1,4 @@
1
- import { Button, Text } from '@servicetitan/anvil2';
1
+ import { Button, Flex, Text } from '@servicetitan/anvil2';
2
2
  import IconRefresh from '@servicetitan/anvil2/assets/icons/material/round/refresh.svg';
3
3
  import { useDependencies } from '@servicetitan/react-ioc';
4
4
  import {
@@ -26,21 +26,24 @@ export const ChatMessageTemplateUser: FC<PropsWithChildren<IChatMessageProps>> =
26
26
  isError={isError}
27
27
  messageFooter={
28
28
  isError ? (
29
- <Text
30
- variant="eyebrow"
31
- className="c-red-600"
32
- data-cy="titan-chat-message-error"
33
- >
34
- Message not delivered. Retry
29
+ <Flex direction="row" alignItems="center" gap="1">
30
+ <Text
31
+ variant="eyebrow"
32
+ className="c-danger"
33
+ data-cy="titan-chat-message-error"
34
+ >
35
+ Message not delivered. Retry
36
+ </Text>
35
37
  <Button
36
38
  icon={IconRefresh}
37
39
  appearance="ghost"
38
40
  size="small"
41
+ className="c-danger"
39
42
  aria-label="Retry send message"
40
43
  onClick={handleRetry}
41
44
  data-cy="titan-chat-message-error-retry"
42
45
  />
43
- </Text>
46
+ </Flex>
44
47
  ) : !omitTimestamp ? (
45
48
  <MessageFooter timestamp={message.timestamp} />
46
49
  ) : null