@servicetitan/titan-chatbot-api 3.0.0

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 (160) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/api-client/__mocks__/chatbot-api-client.mock.d.ts +11 -0
  3. package/dist/api-client/__mocks__/chatbot-api-client.mock.d.ts.map +1 -0
  4. package/dist/api-client/__mocks__/chatbot-api-client.mock.js +47 -0
  5. package/dist/api-client/__mocks__/chatbot-api-client.mock.js.map +1 -0
  6. package/dist/api-client/base/chatbot-api-client.d.ts +27 -0
  7. package/dist/api-client/base/chatbot-api-client.d.ts.map +1 -0
  8. package/dist/api-client/base/chatbot-api-client.js +10 -0
  9. package/dist/api-client/base/chatbot-api-client.js.map +1 -0
  10. package/dist/api-client/help-center/__tests__/converter-from-models.test.d.ts +2 -0
  11. package/dist/api-client/help-center/__tests__/converter-from-models.test.d.ts.map +1 -0
  12. package/dist/api-client/help-center/__tests__/converter-from-models.test.js +34 -0
  13. package/dist/api-client/help-center/__tests__/converter-from-models.test.js.map +1 -0
  14. package/dist/api-client/help-center/__tests__/converter-to-models.test.d.ts +2 -0
  15. package/dist/api-client/help-center/__tests__/converter-to-models.test.d.ts.map +1 -0
  16. package/dist/api-client/help-center/__tests__/converter-to-models.test.js +82 -0
  17. package/dist/api-client/help-center/__tests__/converter-to-models.test.js.map +1 -0
  18. package/dist/api-client/help-center/chatbot-api-client.d.ts +31 -0
  19. package/dist/api-client/help-center/chatbot-api-client.d.ts.map +1 -0
  20. package/dist/api-client/help-center/chatbot-api-client.js +90 -0
  21. package/dist/api-client/help-center/chatbot-api-client.js.map +1 -0
  22. package/dist/api-client/help-center/converter-from-models.d.ts +13 -0
  23. package/dist/api-client/help-center/converter-from-models.d.ts.map +1 -0
  24. package/dist/api-client/help-center/converter-from-models.js +113 -0
  25. package/dist/api-client/help-center/converter-from-models.js.map +1 -0
  26. package/dist/api-client/help-center/converter-to-models.d.ts +13 -0
  27. package/dist/api-client/help-center/converter-to-models.d.ts.map +1 -0
  28. package/dist/api-client/help-center/converter-to-models.js +95 -0
  29. package/dist/api-client/help-center/converter-to-models.js.map +1 -0
  30. package/dist/api-client/help-center/index.d.ts +2 -0
  31. package/dist/api-client/help-center/index.d.ts.map +1 -0
  32. package/dist/api-client/help-center/index.js +2 -0
  33. package/dist/api-client/help-center/index.js.map +1 -0
  34. package/dist/api-client/help-center/native-client.d.ts +1260 -0
  35. package/dist/api-client/help-center/native-client.d.ts.map +1 -0
  36. package/dist/api-client/help-center/native-client.js +6169 -0
  37. package/dist/api-client/help-center/native-client.js.map +1 -0
  38. package/dist/api-client/index.d.ts +8 -0
  39. package/dist/api-client/index.d.ts.map +1 -0
  40. package/dist/api-client/index.js +8 -0
  41. package/dist/api-client/index.js.map +1 -0
  42. package/dist/api-client/models/__mocks__/models.mock.d.ts +13 -0
  43. package/dist/api-client/models/__mocks__/models.mock.d.ts.map +1 -0
  44. package/dist/api-client/models/__mocks__/models.mock.js +114 -0
  45. package/dist/api-client/models/__mocks__/models.mock.js.map +1 -0
  46. package/dist/api-client/models/index.d.ts +22 -0
  47. package/dist/api-client/models/index.d.ts.map +1 -0
  48. package/dist/api-client/models/index.js +15 -0
  49. package/dist/api-client/models/index.js.map +1 -0
  50. package/dist/api-client/titan-chat/chatbot-api-client.d.ts +34 -0
  51. package/dist/api-client/titan-chat/chatbot-api-client.d.ts.map +1 -0
  52. package/dist/api-client/titan-chat/chatbot-api-client.js +72 -0
  53. package/dist/api-client/titan-chat/chatbot-api-client.js.map +1 -0
  54. package/dist/api-client/titan-chat/index.d.ts +2 -0
  55. package/dist/api-client/titan-chat/index.d.ts.map +1 -0
  56. package/dist/api-client/titan-chat/index.js +2 -0
  57. package/dist/api-client/titan-chat/index.js.map +1 -0
  58. package/dist/api-client/titan-chat/native-client.d.ts +225 -0
  59. package/dist/api-client/titan-chat/native-client.d.ts.map +1 -0
  60. package/dist/api-client/titan-chat/native-client.js +931 -0
  61. package/dist/api-client/titan-chat/native-client.js.map +1 -0
  62. package/dist/api-client/utils/model-utils.d.ts +4 -0
  63. package/dist/api-client/utils/model-utils.d.ts.map +1 -0
  64. package/dist/api-client/utils/model-utils.js +58 -0
  65. package/dist/api-client/utils/model-utils.js.map +1 -0
  66. package/dist/index.d.ts +6 -0
  67. package/dist/index.d.ts.map +1 -0
  68. package/dist/index.js +7 -0
  69. package/dist/index.js.map +1 -0
  70. package/dist/models/chatbot-customizations.d.ts +15 -0
  71. package/dist/models/chatbot-customizations.d.ts.map +1 -0
  72. package/dist/models/chatbot-customizations.js +2 -0
  73. package/dist/models/chatbot-customizations.js.map +1 -0
  74. package/dist/models/index.d.ts +2 -0
  75. package/dist/models/index.d.ts.map +1 -0
  76. package/dist/models/index.js +2 -0
  77. package/dist/models/index.js.map +1 -0
  78. package/dist/stores/__tests__/chatbot-ui-backend.store.test.d.ts +2 -0
  79. package/dist/stores/__tests__/chatbot-ui-backend.store.test.d.ts.map +1 -0
  80. package/dist/stores/__tests__/chatbot-ui-backend.store.test.js +341 -0
  81. package/dist/stores/__tests__/chatbot-ui-backend.store.test.js.map +1 -0
  82. package/dist/stores/__tests__/chatbot-ui.store.test.d.ts +2 -0
  83. package/dist/stores/__tests__/chatbot-ui.store.test.d.ts.map +1 -0
  84. package/dist/stores/__tests__/chatbot-ui.store.test.js +166 -0
  85. package/dist/stores/__tests__/chatbot-ui.store.test.js.map +1 -0
  86. package/dist/stores/__tests__/filter.store.test.d.ts +2 -0
  87. package/dist/stores/__tests__/filter.store.test.d.ts.map +1 -0
  88. package/dist/stores/__tests__/filter.store.test.js +316 -0
  89. package/dist/stores/__tests__/filter.store.test.js.map +1 -0
  90. package/dist/stores/__tests__/initialize.store.test.d.ts +2 -0
  91. package/dist/stores/__tests__/initialize.store.test.d.ts.map +1 -0
  92. package/dist/stores/__tests__/initialize.store.test.js +54 -0
  93. package/dist/stores/__tests__/initialize.store.test.js.map +1 -0
  94. package/dist/stores/chatbot-ui-backend.store.d.ts +61 -0
  95. package/dist/stores/chatbot-ui-backend.store.d.ts.map +1 -0
  96. package/dist/stores/chatbot-ui-backend.store.js +396 -0
  97. package/dist/stores/chatbot-ui-backend.store.js.map +1 -0
  98. package/dist/stores/chatbot-ui.store.d.ts +25 -0
  99. package/dist/stores/chatbot-ui.store.d.ts.map +1 -0
  100. package/dist/stores/chatbot-ui.store.js +87 -0
  101. package/dist/stores/chatbot-ui.store.js.map +1 -0
  102. package/dist/stores/filter.store.d.ts +30 -0
  103. package/dist/stores/filter.store.d.ts.map +1 -0
  104. package/dist/stores/filter.store.js +334 -0
  105. package/dist/stores/filter.store.js.map +1 -0
  106. package/dist/stores/index.d.ts +4 -0
  107. package/dist/stores/index.d.ts.map +1 -0
  108. package/dist/stores/index.js +4 -0
  109. package/dist/stores/index.js.map +1 -0
  110. package/dist/stores/initialize.store.d.ts +17 -0
  111. package/dist/stores/initialize.store.d.ts.map +1 -0
  112. package/dist/stores/initialize.store.js +98 -0
  113. package/dist/stores/initialize.store.js.map +1 -0
  114. package/dist/utils/__tests__/axios-utils.test.d.ts +2 -0
  115. package/dist/utils/__tests__/axios-utils.test.d.ts.map +1 -0
  116. package/dist/utils/__tests__/axios-utils.test.js +33 -0
  117. package/dist/utils/__tests__/axios-utils.test.js.map +1 -0
  118. package/dist/utils/axios-utils.d.ts +5 -0
  119. package/dist/utils/axios-utils.d.ts.map +1 -0
  120. package/dist/utils/axios-utils.js +23 -0
  121. package/dist/utils/axios-utils.js.map +1 -0
  122. package/dist/utils/test-utils.d.ts +5 -0
  123. package/dist/utils/test-utils.d.ts.map +1 -0
  124. package/dist/utils/test-utils.js +17 -0
  125. package/dist/utils/test-utils.js.map +1 -0
  126. package/package.json +45 -0
  127. package/src/api-client/__mocks__/chatbot-api-client.mock.ts +11 -0
  128. package/src/api-client/base/chatbot-api-client.ts +33 -0
  129. package/src/api-client/help-center/__tests__/converter-from-models.test.ts +41 -0
  130. package/src/api-client/help-center/__tests__/converter-to-models.test.ts +89 -0
  131. package/src/api-client/help-center/chatbot-api-client.ts +107 -0
  132. package/src/api-client/help-center/converter-from-models.ts +132 -0
  133. package/src/api-client/help-center/converter-to-models.ts +124 -0
  134. package/src/api-client/help-center/index.ts +1 -0
  135. package/src/api-client/help-center/native-client.ts +5662 -0
  136. package/src/api-client/index.ts +12 -0
  137. package/src/api-client/models/__mocks__/models.mock.ts +141 -0
  138. package/src/api-client/models/index.ts +48 -0
  139. package/src/api-client/titan-chat/chatbot-api-client.ts +77 -0
  140. package/src/api-client/titan-chat/index.ts +1 -0
  141. package/src/api-client/titan-chat/native-client.ts +826 -0
  142. package/src/api-client/utils/model-utils.ts +68 -0
  143. package/src/cypress.d.ts +10 -0
  144. package/src/index.ts +6 -0
  145. package/src/models/chatbot-customizations.ts +16 -0
  146. package/src/models/index.ts +1 -0
  147. package/src/stores/__tests__/chatbot-ui-backend.store.test.ts +426 -0
  148. package/src/stores/__tests__/chatbot-ui.store.test.ts +196 -0
  149. package/src/stores/__tests__/filter.store.test.ts +363 -0
  150. package/src/stores/__tests__/initialize.store.test.ts +73 -0
  151. package/src/stores/chatbot-ui-backend.store.ts +401 -0
  152. package/src/stores/chatbot-ui.store.ts +82 -0
  153. package/src/stores/filter.store.ts +250 -0
  154. package/src/stores/index.ts +12 -0
  155. package/src/stores/initialize.store.ts +62 -0
  156. package/src/utils/__tests__/axios-utils.test.ts +40 -0
  157. package/src/utils/axios-utils.ts +25 -0
  158. package/src/utils/test-utils.ts +22 -0
  159. package/tsconfig.json +19 -0
  160. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,68 @@
1
+ import { Models } from '..';
2
+
3
+ export function createNewSessionModel(): Models.Session {
4
+ return new Models.Session();
5
+ }
6
+
7
+ export function createSelectionsModel(
8
+ filters: Models.IOption[],
9
+ selected: Record<string, string[]>
10
+ ): Models.Selections | undefined {
11
+ const process = (filters: Models.IOption[]): Models.Selections | undefined => {
12
+ let result: Models.Selections | undefined;
13
+ const ensureResult = () => {
14
+ if (!result) {
15
+ result = new Models.Selections({
16
+ subOptions: {},
17
+ });
18
+ }
19
+ };
20
+
21
+ const filterList = filters.filter(
22
+ x => x.type === Models.OptionType.Group && Boolean(x.subOptions?.length)
23
+ );
24
+ for (const filter of filterList) {
25
+ const hasSubFilters = Boolean(
26
+ filter.subOptions?.some(x => Boolean(x.subOptions?.length))
27
+ );
28
+ if (!hasSubFilters) {
29
+ // Leaf filter: just collect selected values
30
+ const values = filter.subOptions
31
+ ?.map(o => o.key)
32
+ .filter(o => selected[filter.key]?.includes(o));
33
+ if (values?.length) {
34
+ ensureResult();
35
+ result!.subOptions![filter.key] = new Models.Selections({ values });
36
+ }
37
+ } else {
38
+ // Non-leaf filter: add selected options as selectables and process sub-filters
39
+ const filterSelectables = filter.subOptions!.filter(
40
+ x =>
41
+ x.type === Models.OptionType.Selectable &&
42
+ Boolean(x.subOptions?.length) &&
43
+ selected[filter.key]?.includes(x.key!)
44
+ );
45
+ if (!filterSelectables.length) {
46
+ continue;
47
+ }
48
+ ensureResult();
49
+ const filterResult = new Models.Selections({
50
+ subOptions: {},
51
+ });
52
+ result!.subOptions![filter.key] = filterResult;
53
+ for (const filterSelectable of filterSelectables) {
54
+ // Process sub-filters: if any sub-filters selected, add them to the result
55
+ const subFilters = filterSelectable.subOptions!;
56
+ const resultSubFilters = process(subFilters);
57
+ filterResult.subOptions![filterSelectable.key] =
58
+ resultSubFilters ??
59
+ new Models.Selections({
60
+ subOptions: {},
61
+ });
62
+ }
63
+ }
64
+ }
65
+ return result;
66
+ };
67
+ return process(filters);
68
+ }
@@ -0,0 +1,10 @@
1
+ // eslint-disable-next-line spaced-comment
2
+ /// <reference types="cypress" />
3
+ declare namespace Cypress {
4
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
5
+ interface Chainable<Subject = any> {
6
+ getCy<E extends Node = HTMLElement>(value: string): Chainable<JQuery<E>>;
7
+ getCy2<E extends Node = HTMLElement>(value: string): Chainable<JQuery<E>>;
8
+ getAnvil<E extends Node = HTMLElement>(value: string, extra?: string): Chainable<JQuery<E>>;
9
+ }
10
+ }
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ /* eslint-disable no-restricted-imports */
2
+ export * from './api-client';
3
+ export * from './stores';
4
+ export * from './models';
5
+ export * as NativeClientHC from './api-client/help-center/native-client';
6
+ export * as NativeClientTC from './api-client/titan-chat/native-client';
@@ -0,0 +1,16 @@
1
+ import { ChatCustomizations, ChatError } from '@servicetitan/titan-chat-ui-common';
2
+
3
+ export type ChatbotCustomizations = ChatCustomizations &
4
+ Partial<{
5
+ error?: {
6
+ recover: (error?: ChatError) => Promise<void>;
7
+ };
8
+ feedback?: {
9
+ title?: string;
10
+ showGuardrailFeedback?: boolean;
11
+ isCommentAlwaysRequired?: boolean;
12
+ };
13
+ filters?: {
14
+ enabled?: boolean;
15
+ };
16
+ }>;
@@ -0,0 +1 @@
1
+ export * from './chatbot-customizations';
@@ -0,0 +1,426 @@
1
+ import { expect } from '@jest/globals';
2
+ import { ILog, Log, LogError, LogInfo, LogWarning } from '@servicetitan/log-service';
3
+ import { Container } from '@servicetitan/react-ioc';
4
+ import {
5
+ ChatError,
6
+ ChatMessageModelText,
7
+ ChatMessageModelWelcome,
8
+ ChatRunState,
9
+ ChatUiEventListener,
10
+ } from '@servicetitan/titan-chat-ui-common';
11
+ import { CHATBOT_API_CLIENT, IChatbotApiClient, Models, ModelsMocks } from '../../api-client';
12
+ import { ChatbotApiClientMock } from '../../api-client/__mocks__/chatbot-api-client.mock';
13
+ import { initTestContainer } from '../../utils/test-utils';
14
+ import { ChatbotUiBackendStore } from '../chatbot-ui-backend.store';
15
+ import { CHATBOT_UI_STORE_TOKEN, ChatbotUiStore } from '../chatbot-ui.store';
16
+
17
+ const WELCOME_MESSAGE =
18
+ 'Hi there! I’m Titan, an AI chatbot powered by Titan Intelligence. I’m here to answer your questions about using ServiceTitan. You can ask me things like "How to merge duplicate customers?". Do you have a question for me?';
19
+
20
+ const initContainer = initTestContainer(ChatbotUiBackendStore, container => {
21
+ container
22
+ .bind<ILog>(Log)
23
+ .to(
24
+ class implements ILog {
25
+ error: (entry: LogError) => void = jest.fn();
26
+ info: (entry: LogInfo) => void = jest.fn();
27
+ warning: (entry: LogWarning) => void = jest.fn();
28
+ }
29
+ )
30
+ .inSingletonScope();
31
+ container
32
+ .bind<IChatbotApiClient>(CHATBOT_API_CLIENT)
33
+ .toConstantValue(new ChatbotApiClientMock());
34
+ container.bind(CHATBOT_UI_STORE_TOKEN).to(ChatbotUiStore).inSingletonScope();
35
+ });
36
+
37
+ describe('[ChatbotUiBackendStore]', () => {
38
+ let container: Container;
39
+ let store: ChatbotUiBackendStore;
40
+ let chatbotApi: ChatbotApiClientMock;
41
+ let chatUiStore: ChatbotUiStore;
42
+ let log: ILog;
43
+
44
+ beforeEach(() => {
45
+ container = initContainer();
46
+ store = container.get(ChatbotUiBackendStore);
47
+ chatbotApi = container.get<IChatbotApiClient>(CHATBOT_API_CLIENT) as ChatbotApiClientMock;
48
+ chatUiStore = container.get<ChatbotUiStore>(CHATBOT_UI_STORE_TOKEN);
49
+ log = container.get<ILog>(Log);
50
+ jest.useFakeTimers();
51
+ jest.setSystemTime(new Date('2000-01-01T00:00:00.000Z'));
52
+ });
53
+
54
+ afterEach(() => {
55
+ jest.useRealTimers();
56
+ jest.clearAllMocks();
57
+ });
58
+
59
+ describe('with subscription', () => {
60
+ let spyOn: jest.SpyInstance;
61
+ let spyOff: jest.SpyInstance;
62
+
63
+ beforeEach(() => {
64
+ spyOn = jest.spyOn(chatUiStore, 'on');
65
+ spyOff = jest.spyOn(chatUiStore, 'off');
66
+ });
67
+
68
+ test('should subscribe on external events', () => {
69
+ store.subscribe();
70
+ expect(spyOn).toHaveBeenCalledTimes(7);
71
+ expect(spyOff).toHaveBeenCalledTimes(7);
72
+ });
73
+
74
+ test('should unsubscribe from external events', () => {
75
+ store.unsubscribe();
76
+ expect(chatUiStore.on).toHaveBeenCalledTimes(0);
77
+ expect(chatUiStore.off).toHaveBeenCalledTimes(7);
78
+ });
79
+ });
80
+
81
+ describe('with run', () => {
82
+ const runChatUiEventListener = async <T = void>(
83
+ listener: ChatUiEventListener<T>,
84
+ args: unknown[]
85
+ ) => {
86
+ return new Promise<T>((resolve, reject) => {
87
+ listener.apply(store, [resolve, reject, ...args]);
88
+ });
89
+ };
90
+
91
+ const runStore = async () => {
92
+ await runChatUiEventListener(store.handleRun, []);
93
+ await jest.advanceTimersToNextTimerAsync(); // Wait for the timer to be set
94
+ };
95
+
96
+ const mockChatbotSession = () => {
97
+ chatbotApi.getOptions.mockResolvedValue(ModelsMocks.mockFrontendModel());
98
+ chatbotApi.postSession.mockResolvedValue(ModelsMocks.mockSession());
99
+ chatbotApi.deleteSession.mockResolvedValue(ModelsMocks.mockSession());
100
+ };
101
+
102
+ const mockChatbotAnswer = (answer: string) => {
103
+ chatbotApi.postMessage.mockResolvedValue(
104
+ ModelsMocks.mockBotMessage({
105
+ answer,
106
+ })
107
+ );
108
+ };
109
+
110
+ const ensureChatStarted = () => {
111
+ expect(chatUiStore.isAgentTyping).toBe(false);
112
+ expect(chatUiStore.isFilePickerEnabled).toBe(false);
113
+ expect(chatUiStore.file).toBe(undefined);
114
+ expect(chatUiStore.currentFileMessage).toBe(undefined);
115
+ expect(chatUiStore.error).toBe(undefined);
116
+ expect(chatUiStore.endReason).toBe(undefined);
117
+ expect(chatUiStore.timer).toBe(undefined);
118
+ expect(chatUiStore.scrollCounter > 1).toBe(true);
119
+ expect(chatUiStore.status).toBe(ChatRunState.Started);
120
+ };
121
+
122
+ test('should run chatbot without stored session', async () => {
123
+ store.subscribe();
124
+ mockChatbotSession();
125
+
126
+ await runStore();
127
+
128
+ ensureChatStarted();
129
+ expect(chatUiStore.messages.length).toBe(1);
130
+ expect(chatUiStore.messages[0].type).toBe('welcome');
131
+ expect((chatUiStore.messages[0] as ChatMessageModelText).message).toBe(WELCOME_MESSAGE);
132
+
133
+ /*
134
+ * We don't have session yet because we've just initialized the chat with welcome message
135
+ * and we don't do any chatbotApi.postSession call
136
+ */
137
+ expect(store.session).toBeUndefined();
138
+ expect(chatbotApi.postSession).toHaveBeenCalledTimes(0);
139
+
140
+ mockChatbotAnswer('bot answer');
141
+ await chatUiStore.sendMessageText('user question');
142
+ expect(chatUiStore.messages.length).toBe(3);
143
+ expect(chatUiStore.messages[0].type).toBe('welcome');
144
+ expect(chatUiStore.messages[1].type).toBe('message');
145
+ expect(chatUiStore.messages[2].type).toBe('message');
146
+ expect(chatUiStore.messages[1].participant.isAgent).toBe(false);
147
+ expect(chatUiStore.messages[2].participant.isAgent).toBe(true);
148
+ expect((chatUiStore.messages[1] as ChatMessageModelText).message).toBe('user question');
149
+ expect((chatUiStore.messages[2] as ChatMessageModelText).message).toBe('bot answer');
150
+ expect(log.error).toHaveBeenCalledTimes(0);
151
+ });
152
+
153
+ test('should run chatbot with stored session with expiration', async () => {
154
+ mockChatbotSession();
155
+
156
+ await jest.advanceTimersByTimeAsync(1000 * 60 * 60 * 16);
157
+ await runStore();
158
+ ensureChatStarted();
159
+ expect(store.session).not.toBeDefined();
160
+ expect(chatUiStore.messages.length).toBe(1);
161
+ expect(chatbotApi.postSession).toHaveBeenCalledTimes(0);
162
+ expect(log.error).toHaveBeenCalledTimes(0);
163
+ });
164
+
165
+ test('should destroy', async () => {
166
+ mockChatbotSession();
167
+
168
+ await runStore();
169
+ store.session = ModelsMocks.mockSession();
170
+ await runChatUiEventListener(store.handleDestroy, []);
171
+
172
+ expect(chatbotApi.postSession).toHaveBeenCalledTimes(0);
173
+ expect(chatbotApi.deleteSession).toHaveBeenCalledTimes(1);
174
+ expect(chatUiStore.status).toBe(ChatRunState.Offline);
175
+ expect(store.session).toBeUndefined();
176
+ expect(log.error).toHaveBeenCalledTimes(0);
177
+ });
178
+
179
+ test('should restart', async () => {
180
+ mockChatbotSession();
181
+
182
+ await runStore();
183
+ store.session = ModelsMocks.mockSession();
184
+ await runChatUiEventListener(store.handleRestart, []);
185
+
186
+ expect(chatbotApi.postSession).toHaveBeenCalledTimes(0);
187
+ expect(chatbotApi.deleteSession).toHaveBeenCalledTimes(1);
188
+ expect(chatUiStore.status).toBe(ChatRunState.Started);
189
+ expect(log.error).toHaveBeenCalledTimes(0);
190
+ });
191
+
192
+ describe('with messages', () => {
193
+ test('should send message', async () => {
194
+ mockChatbotSession();
195
+ mockChatbotAnswer('bot answer');
196
+
197
+ await runStore();
198
+
199
+ await chatUiStore.sendMessageText('user question');
200
+ const message = chatUiStore.messages.at(-1)! as ChatMessageModelText;
201
+ await runChatUiEventListener(store.handleMessageSend, [message]);
202
+
203
+ expect(chatUiStore.messages.length).toBe(3);
204
+ expect((chatUiStore.messages[0] as ChatMessageModelText).message).toBe(
205
+ WELCOME_MESSAGE
206
+ );
207
+ expect((chatUiStore.messages[1] as ChatMessageModelText).message).toBe(
208
+ 'user question'
209
+ );
210
+ expect((chatUiStore.messages[2] as ChatMessageModelText).message).toBe(
211
+ 'bot answer'
212
+ );
213
+ expect(log.error).toHaveBeenCalledTimes(0);
214
+ });
215
+
216
+ test('should send message with error', async () => {
217
+ mockChatbotSession();
218
+ chatbotApi.postMessage.mockRejectedValue(new Error('error'));
219
+
220
+ await chatUiStore.sendMessageText('user question');
221
+ const message = chatUiStore.messages.at(-1)! as ChatMessageModelText;
222
+ await runChatUiEventListener(store.handleMessageSend, [message]);
223
+
224
+ expect(chatbotApi.postSession).toHaveBeenCalled();
225
+ expect(chatbotApi.postMessage).toHaveBeenCalled();
226
+ expect(chatUiStore.isError).toBe(true);
227
+ expect(log.error).toHaveBeenCalledWith({
228
+ category: 'TitanChatbot',
229
+ code: 'TitanChatbot_FailedToSendMessage',
230
+ error: new Error('error'),
231
+ message: 'Failed to send message',
232
+ });
233
+ });
234
+
235
+ test('should send message retry', async () => {
236
+ mockChatbotSession();
237
+ const spyMessage = jest.spyOn(chatbotApi, 'postMessage');
238
+
239
+ // Retry with non-text message should not call "message"
240
+ const messageNonText = ModelsMocks.mockChatMessageModel<ChatMessageModelWelcome>({
241
+ type: 'welcome',
242
+ });
243
+ await runChatUiEventListener(store.handleMessageSendRetry, [messageNonText]);
244
+ expect(spyMessage).not.toHaveBeenCalled();
245
+
246
+ // Retry with text message should call "message"
247
+ const message = ModelsMocks.mockChatMessageModel<ChatMessageModelText>({
248
+ message: 'message',
249
+ });
250
+ await runChatUiEventListener(store.handleMessageSendRetry, [message]);
251
+ expect(spyMessage).toHaveBeenCalledWith(
252
+ ModelsMocks.mockUserMessage({
253
+ context: undefined,
254
+ selections: undefined,
255
+ question: 'message',
256
+ }),
257
+ expect.any(AbortSignal)
258
+ );
259
+ });
260
+ });
261
+
262
+ describe('with session feedback', () => {
263
+ const createSessionFeedback = () =>
264
+ ModelsMocks.mockFeedback({
265
+ sessionId: 1,
266
+ messageId: undefined,
267
+ rating: Models.FeedbackRatings.ThumbsUp,
268
+ description: 'description',
269
+ });
270
+
271
+ test('should send feedback', async () => {
272
+ mockChatbotSession();
273
+
274
+ chatbotApi.postFeedback.mockResolvedValueOnce(createSessionFeedback());
275
+
276
+ const { feedback, state } = await runChatUiEventListener(
277
+ store.handleSessionFeedback,
278
+ [createSessionFeedback()]
279
+ );
280
+
281
+ expect(state).toBe(Models.ChatbotFeedbackState.Success);
282
+ expect(feedback).toEqual(createSessionFeedback());
283
+ expect(chatbotApi.postSession).toHaveBeenCalled();
284
+ expect(chatbotApi.postFeedback).toHaveBeenCalledWith(
285
+ createSessionFeedback(),
286
+ expect.any(AbortSignal)
287
+ );
288
+ });
289
+
290
+ test('should send feedback with session error', async () => {
291
+ chatbotApi.postSession.mockRejectedValueOnce('session error');
292
+ chatbotApi.postFeedback.mockResolvedValueOnce(createSessionFeedback());
293
+
294
+ const { feedback, state } = await runChatUiEventListener(
295
+ store.handleSessionFeedback,
296
+ [createSessionFeedback()]
297
+ );
298
+
299
+ expect(state).toBe(Models.ChatbotFeedbackState.Failure);
300
+ expect(feedback).toEqual(createSessionFeedback());
301
+ expect(chatbotApi.postSession).toHaveBeenCalled();
302
+ expect(chatbotApi.postFeedback).not.toHaveBeenCalled();
303
+ expect(chatUiStore.isError).toBe(true);
304
+ expect(log.error).toHaveBeenCalledWith({
305
+ category: 'TitanChatbot',
306
+ code: 'TitanChatbot_FailedToSendFeedback',
307
+ error: 'session error',
308
+ message: 'Failed to send feedback',
309
+ });
310
+ });
311
+
312
+ test('should send feedback with error', async () => {
313
+ mockChatbotSession();
314
+ chatbotApi.postSession.mockReset().mockRejectedValue(new Error('session error'));
315
+ chatbotApi.postFeedback.mockResolvedValueOnce(createSessionFeedback());
316
+
317
+ const { feedback, state } = await runChatUiEventListener(
318
+ store.handleSessionFeedback,
319
+ [createSessionFeedback()]
320
+ );
321
+
322
+ expect(state).toBe(Models.ChatbotFeedbackState.Failure);
323
+ expect(feedback).toEqual(createSessionFeedback());
324
+ expect(chatbotApi.postSession).toHaveBeenCalled();
325
+ expect(chatbotApi.postFeedback).not.toHaveBeenCalled();
326
+ expect(chatUiStore.isError).toBe(true);
327
+ expect(log.error).toHaveBeenCalledWith({
328
+ category: 'TitanChatbot',
329
+ code: 'TitanChatbot_FailedToSendFeedback',
330
+ error: new Error('session error'),
331
+ message: 'Failed to send feedback',
332
+ });
333
+ });
334
+ });
335
+
336
+ describe('with message feedback', () => {
337
+ const createMessageFeedback = () =>
338
+ ModelsMocks.mockFeedback({
339
+ sessionId: 1,
340
+ messageId: 1,
341
+ description: 'description',
342
+ linkUrl: 'linkUrl',
343
+ options: [Models.FeedbackOptions.Unclear, Models.FeedbackOptions.Incorrect],
344
+ rating: Models.FeedbackRatings.ThumbsUp,
345
+ });
346
+ const createLogError = (errorMessage: string) => ({
347
+ category: 'TitanChatbot',
348
+ code: 'TitanChatbot_FailedToSendMessageFeedback',
349
+ error: errorMessage,
350
+ message: 'Failed to send message feedback',
351
+ });
352
+
353
+ test('should assert message feedback 1', async () => {
354
+ const { feedback, state } = await runChatUiEventListener(
355
+ store.handleMessageFeedback,
356
+ []
357
+ );
358
+ expect(feedback).toBeUndefined();
359
+ expect(state).toBe(Models.ChatbotFeedbackState.Failure);
360
+ expect(chatUiStore.isError).toBe(true);
361
+ expect(chatUiStore.error?.message).toBe('Failed to send message feedback');
362
+ expect(log.error).toHaveBeenCalledWith({
363
+ category: 'TitanChatbot',
364
+ code: 'TitanChatbot_FailedToSendMessageFeedback',
365
+ error: new ChatError('Message feedback is missing.'),
366
+ message: 'Failed to send message feedback',
367
+ });
368
+ expect(chatbotApi.postFeedback).not.toHaveBeenCalled();
369
+ });
370
+
371
+ test('should assert message feedback 3', async () => {
372
+ chatbotApi.postSession.mockRejectedValueOnce('session error');
373
+ const { feedback, state } = await runChatUiEventListener(
374
+ store.handleMessageFeedback,
375
+ [createMessageFeedback()]
376
+ );
377
+ expect(state).toBe(Models.ChatbotFeedbackState.Failure);
378
+ expect(feedback).toEqual(createMessageFeedback());
379
+ expect(chatUiStore.isError).toBe(true);
380
+ expect(chatUiStore.error?.message).toBe('Failed to send message feedback');
381
+ expect(log.error).toHaveBeenCalledWith({
382
+ category: 'TitanChatbot',
383
+ code: 'TitanChatbot_FailedToSendMessageFeedback',
384
+ error: 'session error',
385
+ message: 'Failed to send message feedback',
386
+ });
387
+ expect(chatbotApi.postFeedback).not.toHaveBeenCalled();
388
+ });
389
+
390
+ test('should send message feedback', async () => {
391
+ mockChatbotSession();
392
+ chatbotApi.postFeedback.mockResolvedValueOnce(createMessageFeedback());
393
+
394
+ const feedback = createMessageFeedback();
395
+ await runChatUiEventListener(store.handleMessageFeedback, [feedback]);
396
+
397
+ expect(chatbotApi.postFeedback).toHaveBeenCalledWith(
398
+ {
399
+ ...feedback,
400
+ },
401
+ expect.any(AbortSignal)
402
+ );
403
+ });
404
+
405
+ test('should send message feedback with session error', async () => {
406
+ chatbotApi.postSession.mockRejectedValueOnce('session error');
407
+ chatbotApi.postFeedback.mockResolvedValueOnce(createMessageFeedback());
408
+ const feedback = createMessageFeedback();
409
+ await runChatUiEventListener(store.handleMessageFeedback, [feedback]);
410
+
411
+ expect(chatbotApi.postFeedback).not.toHaveBeenCalled();
412
+ expect(log.error).toHaveBeenCalledWith(createLogError('session error'));
413
+ });
414
+
415
+ test('should send message feedback with feedback error', async () => {
416
+ mockChatbotSession();
417
+ chatbotApi.postFeedback.mockRejectedValueOnce('feedback error');
418
+ const feedback = createMessageFeedback();
419
+ await runChatUiEventListener(store.handleMessageFeedback, [feedback]);
420
+
421
+ expect(chatbotApi.postFeedback).toHaveBeenCalledTimes(1);
422
+ expect(log.error).toHaveBeenCalledWith(createLogError('feedback error'));
423
+ });
424
+ });
425
+ });
426
+ });