@product7/feedback-sdk 1.2.5 → 1.2.6
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.
- package/README.md +176 -7
- package/dist/README.md +176 -7
- package/dist/feedback-sdk.js +1603 -125
- package/dist/feedback-sdk.js.map +1 -1
- package/dist/feedback-sdk.min.js +1 -1
- package/dist/feedback-sdk.min.js.map +1 -1
- package/package.json +1 -1
- package/src/core/APIService.js +536 -0
- package/src/core/FeedbackSDK.js +48 -0
- package/src/core/WebSocketService.js +273 -0
- package/src/styles/messengerStyles.js +100 -0
- package/src/styles/styles.js +2 -2
- package/src/widgets/BaseWidget.js +96 -0
- package/src/widgets/MessengerWidget.js +374 -89
- package/src/widgets/messenger/MessengerState.js +12 -0
- package/src/widgets/messenger/views/ChatView.js +121 -16
- package/src/widgets/messenger/views/ConversationsView.js +12 -11
- package/src/widgets/messenger/views/HomeView.js +23 -1
package/dist/feedback-sdk.js
CHANGED
|
@@ -139,6 +139,124 @@
|
|
|
139
139
|
},
|
|
140
140
|
];
|
|
141
141
|
|
|
142
|
+
// Mock conversations for development
|
|
143
|
+
const MOCK_CONVERSATIONS = [
|
|
144
|
+
{
|
|
145
|
+
id: 'conv_1',
|
|
146
|
+
subject: 'Question about pricing',
|
|
147
|
+
status: 'open',
|
|
148
|
+
last_message_at: new Date(Date.now() - 49 * 60 * 1000).toISOString(),
|
|
149
|
+
created_at: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(),
|
|
150
|
+
unread: 1,
|
|
151
|
+
assigned_user: {
|
|
152
|
+
id: 'user_1',
|
|
153
|
+
name: 'Sarah',
|
|
154
|
+
avatar: null,
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
id: 'conv_2',
|
|
159
|
+
subject: 'Feature request',
|
|
160
|
+
status: 'open',
|
|
161
|
+
last_message_at: new Date(Date.now() - 6 * 24 * 60 * 60 * 1000).toISOString(),
|
|
162
|
+
created_at: new Date(Date.now() - 10 * 24 * 60 * 60 * 1000).toISOString(),
|
|
163
|
+
unread: 0,
|
|
164
|
+
assigned_user: {
|
|
165
|
+
id: 'user_2',
|
|
166
|
+
name: 'Tom',
|
|
167
|
+
avatar: null,
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
];
|
|
171
|
+
|
|
172
|
+
// Mock messages for development
|
|
173
|
+
const MOCK_MESSAGES = {
|
|
174
|
+
conv_1: [
|
|
175
|
+
{
|
|
176
|
+
id: 'msg_1',
|
|
177
|
+
content: "Hi there! 👋 I'm Sarah. How can I help you today?",
|
|
178
|
+
sender_type: 'agent',
|
|
179
|
+
sender_name: 'Sarah',
|
|
180
|
+
sender_avatar: null,
|
|
181
|
+
created_at: new Date(Date.now() - 50 * 60 * 1000).toISOString(),
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
id: 'msg_2',
|
|
185
|
+
content: 'Hi! I have a question about your enterprise pricing.',
|
|
186
|
+
sender_type: 'customer',
|
|
187
|
+
created_at: new Date(Date.now() - 49 * 60 * 1000).toISOString(),
|
|
188
|
+
},
|
|
189
|
+
],
|
|
190
|
+
conv_2: [
|
|
191
|
+
{
|
|
192
|
+
id: 'msg_3',
|
|
193
|
+
content: "Hello! I'm Tom from the product team.",
|
|
194
|
+
sender_type: 'agent',
|
|
195
|
+
sender_name: 'Tom',
|
|
196
|
+
sender_avatar: null,
|
|
197
|
+
created_at: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(),
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
id: 'msg_4',
|
|
201
|
+
content: 'I would love to see a dark mode feature!',
|
|
202
|
+
sender_type: 'customer',
|
|
203
|
+
created_at: new Date(Date.now() - 6 * 24 * 60 * 60 * 1000 - 30 * 60 * 1000).toISOString(),
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
id: 'msg_5',
|
|
207
|
+
content: "Great suggestion! That feature will be available next week. I'll let you know when it's ready.",
|
|
208
|
+
sender_type: 'agent',
|
|
209
|
+
sender_name: 'Tom',
|
|
210
|
+
sender_avatar: null,
|
|
211
|
+
created_at: new Date(Date.now() - 6 * 24 * 60 * 60 * 1000).toISOString(),
|
|
212
|
+
},
|
|
213
|
+
],
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
// Mock help collections for development
|
|
217
|
+
const MOCK_HELP_COLLECTIONS = [
|
|
218
|
+
{
|
|
219
|
+
id: 'collection_1',
|
|
220
|
+
title: 'Product Overview',
|
|
221
|
+
description: 'See how your AI-first customer service solution works.',
|
|
222
|
+
articleCount: 24,
|
|
223
|
+
icon: 'ph-book-open',
|
|
224
|
+
url: '#',
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
id: 'collection_2',
|
|
228
|
+
title: 'Getting Started',
|
|
229
|
+
description: 'Everything you need to know to get started with Product7.',
|
|
230
|
+
articleCount: 30,
|
|
231
|
+
icon: 'ph-rocket',
|
|
232
|
+
url: '#',
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
id: 'collection_3',
|
|
236
|
+
title: 'AI Agent',
|
|
237
|
+
description: 'Resolving customer questions instantly and accurately—from live chat to email.',
|
|
238
|
+
articleCount: 82,
|
|
239
|
+
icon: 'ph-robot',
|
|
240
|
+
url: '#',
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
id: 'collection_4',
|
|
244
|
+
title: 'Channels',
|
|
245
|
+
description: 'Enabling the channels you use to communicate with customers, all from the Inbox.',
|
|
246
|
+
articleCount: 45,
|
|
247
|
+
icon: 'ph-chat-circle',
|
|
248
|
+
url: '#',
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
id: 'collection_5',
|
|
252
|
+
title: 'Billing & Payments',
|
|
253
|
+
description: 'Manage your subscription, invoices, and payment methods.',
|
|
254
|
+
articleCount: 12,
|
|
255
|
+
icon: 'ph-credit-card',
|
|
256
|
+
url: '#',
|
|
257
|
+
},
|
|
258
|
+
];
|
|
259
|
+
|
|
142
260
|
// Mock surveys for development
|
|
143
261
|
const MOCK_SURVEYS = [
|
|
144
262
|
{
|
|
@@ -560,6 +678,424 @@
|
|
|
560
678
|
}
|
|
561
679
|
}
|
|
562
680
|
|
|
681
|
+
// ==========================================
|
|
682
|
+
// MESSENGER / CHAT ENDPOINTS
|
|
683
|
+
// ==========================================
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Get messenger settings
|
|
687
|
+
* @returns {Promise<Object>} Messenger settings
|
|
688
|
+
*/
|
|
689
|
+
async getMessengerSettings() {
|
|
690
|
+
if (!this.isSessionValid()) {
|
|
691
|
+
await this.init();
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
if (this.mock) {
|
|
695
|
+
return {
|
|
696
|
+
status: true,
|
|
697
|
+
data: {
|
|
698
|
+
enabled: true,
|
|
699
|
+
greeting_message: 'Hi there! How can we help you today?',
|
|
700
|
+
team_name: 'Support Team',
|
|
701
|
+
response_time: 'Usually replies within a few minutes',
|
|
702
|
+
},
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
return this._makeRequest('/widget/messenger/settings', {
|
|
707
|
+
method: 'GET',
|
|
708
|
+
headers: { Authorization: `Bearer ${this.sessionToken}` },
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Check if agents are online
|
|
714
|
+
* @returns {Promise<Object>} Agent availability status
|
|
715
|
+
*/
|
|
716
|
+
async checkAgentsOnline() {
|
|
717
|
+
if (!this.isSessionValid()) {
|
|
718
|
+
await this.init();
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
if (this.mock) {
|
|
722
|
+
return {
|
|
723
|
+
status: true,
|
|
724
|
+
data: {
|
|
725
|
+
agents_online: true,
|
|
726
|
+
online_count: 2,
|
|
727
|
+
response_time: 'Usually replies within a few minutes',
|
|
728
|
+
},
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
return this._makeRequest('/widget/messenger/agents/online', {
|
|
733
|
+
method: 'GET',
|
|
734
|
+
headers: { Authorization: `Bearer ${this.sessionToken}` },
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* Get all conversations for the current contact
|
|
740
|
+
* @param {Object} options - Query options
|
|
741
|
+
* @param {number} options.page - Page number
|
|
742
|
+
* @param {number} options.limit - Items per page
|
|
743
|
+
* @returns {Promise<Object>} Conversations list
|
|
744
|
+
*/
|
|
745
|
+
async getConversations(options = {}) {
|
|
746
|
+
if (!this.isSessionValid()) {
|
|
747
|
+
await this.init();
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
if (this.mock) {
|
|
751
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
752
|
+
return {
|
|
753
|
+
status: true,
|
|
754
|
+
data: MOCK_CONVERSATIONS,
|
|
755
|
+
meta: { total: MOCK_CONVERSATIONS.length, page: 1, limit: 20 },
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
const params = new URLSearchParams();
|
|
760
|
+
if (options.page) params.append('page', options.page);
|
|
761
|
+
if (options.limit) params.append('limit', options.limit);
|
|
762
|
+
|
|
763
|
+
const endpoint = `/widget/messenger/conversations${params.toString() ? '?' + params.toString() : ''}`;
|
|
764
|
+
return this._makeRequest(endpoint, {
|
|
765
|
+
method: 'GET',
|
|
766
|
+
headers: { Authorization: `Bearer ${this.sessionToken}` },
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* Get a single conversation with messages
|
|
772
|
+
* @param {string} conversationId - Conversation ID
|
|
773
|
+
* @returns {Promise<Object>} Conversation with messages
|
|
774
|
+
*/
|
|
775
|
+
async getConversation(conversationId) {
|
|
776
|
+
if (!this.isSessionValid()) {
|
|
777
|
+
await this.init();
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
if (this.mock) {
|
|
781
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
782
|
+
const conv = MOCK_CONVERSATIONS.find((c) => c.id === conversationId);
|
|
783
|
+
return {
|
|
784
|
+
status: true,
|
|
785
|
+
data: {
|
|
786
|
+
...conv,
|
|
787
|
+
messages: MOCK_MESSAGES[conversationId] || [],
|
|
788
|
+
},
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
return this._makeRequest(`/widget/messenger/conversations/${conversationId}`, {
|
|
793
|
+
method: 'GET',
|
|
794
|
+
headers: { Authorization: `Bearer ${this.sessionToken}` },
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
/**
|
|
799
|
+
* Get messages for a conversation
|
|
800
|
+
* @param {string} conversationId - Conversation ID
|
|
801
|
+
* @param {Object} options - Query options
|
|
802
|
+
* @returns {Promise<Object>} Messages list
|
|
803
|
+
*/
|
|
804
|
+
async getMessages(conversationId, options = {}) {
|
|
805
|
+
if (!this.isSessionValid()) {
|
|
806
|
+
await this.init();
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
if (this.mock) {
|
|
810
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
811
|
+
return {
|
|
812
|
+
status: true,
|
|
813
|
+
data: MOCK_MESSAGES[conversationId] || [],
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
const params = new URLSearchParams();
|
|
818
|
+
if (options.page) params.append('page', options.page);
|
|
819
|
+
if (options.limit) params.append('limit', options.limit);
|
|
820
|
+
|
|
821
|
+
const endpoint = `/widget/messenger/conversations/${conversationId}/messages${params.toString() ? '?' + params.toString() : ''}`;
|
|
822
|
+
return this._makeRequest(endpoint, {
|
|
823
|
+
method: 'GET',
|
|
824
|
+
headers: { Authorization: `Bearer ${this.sessionToken}` },
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* Start a new conversation
|
|
830
|
+
* @param {Object} data - Conversation data
|
|
831
|
+
* @param {string} data.message - Initial message content
|
|
832
|
+
* @param {string} data.subject - Optional subject
|
|
833
|
+
* @returns {Promise<Object>} Created conversation
|
|
834
|
+
*/
|
|
835
|
+
async startConversation(data) {
|
|
836
|
+
if (!this.isSessionValid()) {
|
|
837
|
+
await this.init();
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
if (this.mock) {
|
|
841
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
842
|
+
const newConv = {
|
|
843
|
+
id: 'conv_' + Date.now(),
|
|
844
|
+
subject: data.subject || 'New conversation',
|
|
845
|
+
status: 'open',
|
|
846
|
+
last_message_at: new Date().toISOString(),
|
|
847
|
+
created_at: new Date().toISOString(),
|
|
848
|
+
messages: [
|
|
849
|
+
{
|
|
850
|
+
id: 'msg_' + Date.now(),
|
|
851
|
+
content: data.message,
|
|
852
|
+
sender_type: 'customer',
|
|
853
|
+
created_at: new Date().toISOString(),
|
|
854
|
+
},
|
|
855
|
+
],
|
|
856
|
+
};
|
|
857
|
+
MOCK_CONVERSATIONS.unshift(newConv);
|
|
858
|
+
MOCK_MESSAGES[newConv.id] = newConv.messages;
|
|
859
|
+
return { status: true, data: newConv };
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
return this._makeRequest('/widget/messenger/conversations', {
|
|
863
|
+
method: 'POST',
|
|
864
|
+
headers: {
|
|
865
|
+
'Content-Type': 'application/json',
|
|
866
|
+
Authorization: `Bearer ${this.sessionToken}`,
|
|
867
|
+
},
|
|
868
|
+
body: JSON.stringify({
|
|
869
|
+
message: data.message,
|
|
870
|
+
subject: data.subject || '',
|
|
871
|
+
}),
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
/**
|
|
876
|
+
* Send a message in a conversation
|
|
877
|
+
* @param {string} conversationId - Conversation ID
|
|
878
|
+
* @param {Object} data - Message data
|
|
879
|
+
* @param {string} data.content - Message content
|
|
880
|
+
* @returns {Promise<Object>} Sent message
|
|
881
|
+
*/
|
|
882
|
+
async sendMessage(conversationId, data) {
|
|
883
|
+
if (!this.isSessionValid()) {
|
|
884
|
+
await this.init();
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
if (this.mock) {
|
|
888
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
889
|
+
const newMessage = {
|
|
890
|
+
id: 'msg_' + Date.now(),
|
|
891
|
+
content: data.content,
|
|
892
|
+
sender_type: 'customer',
|
|
893
|
+
created_at: new Date().toISOString(),
|
|
894
|
+
};
|
|
895
|
+
if (!MOCK_MESSAGES[conversationId]) {
|
|
896
|
+
MOCK_MESSAGES[conversationId] = [];
|
|
897
|
+
}
|
|
898
|
+
MOCK_MESSAGES[conversationId].push(newMessage);
|
|
899
|
+
return { status: true, data: newMessage };
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
return this._makeRequest(`/widget/messenger/conversations/${conversationId}/messages`, {
|
|
903
|
+
method: 'POST',
|
|
904
|
+
headers: {
|
|
905
|
+
'Content-Type': 'application/json',
|
|
906
|
+
Authorization: `Bearer ${this.sessionToken}`,
|
|
907
|
+
},
|
|
908
|
+
body: JSON.stringify({ content: data.content }),
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/**
|
|
913
|
+
* Send typing indicator
|
|
914
|
+
* @param {string} conversationId - Conversation ID
|
|
915
|
+
* @param {boolean} isTyping - Whether user is typing
|
|
916
|
+
* @returns {Promise<Object>} Response
|
|
917
|
+
*/
|
|
918
|
+
async sendTypingIndicator(conversationId, isTyping) {
|
|
919
|
+
if (!this.isSessionValid()) {
|
|
920
|
+
await this.init();
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
if (this.mock) {
|
|
924
|
+
return { status: true };
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
return this._makeRequest(`/widget/messenger/conversations/${conversationId}/typing`, {
|
|
928
|
+
method: 'POST',
|
|
929
|
+
headers: {
|
|
930
|
+
'Content-Type': 'application/json',
|
|
931
|
+
Authorization: `Bearer ${this.sessionToken}`,
|
|
932
|
+
},
|
|
933
|
+
body: JSON.stringify({ is_typing: isTyping }),
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
/**
|
|
938
|
+
* Mark conversation as read
|
|
939
|
+
* @param {string} conversationId - Conversation ID
|
|
940
|
+
* @returns {Promise<Object>} Response
|
|
941
|
+
*/
|
|
942
|
+
async markConversationAsRead(conversationId) {
|
|
943
|
+
if (!this.isSessionValid()) {
|
|
944
|
+
await this.init();
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
if (this.mock) {
|
|
948
|
+
return { status: true };
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
return this._makeRequest(`/widget/messenger/conversations/${conversationId}/read`, {
|
|
952
|
+
method: 'POST',
|
|
953
|
+
headers: { Authorization: `Bearer ${this.sessionToken}` },
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
/**
|
|
958
|
+
* Get unread count
|
|
959
|
+
* @returns {Promise<Object>} Unread count data
|
|
960
|
+
*/
|
|
961
|
+
async getUnreadCount() {
|
|
962
|
+
if (!this.isSessionValid()) {
|
|
963
|
+
await this.init();
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
if (this.mock) {
|
|
967
|
+
const count = MOCK_CONVERSATIONS.reduce((sum, c) => sum + (c.unread || 0), 0);
|
|
968
|
+
return {
|
|
969
|
+
status: true,
|
|
970
|
+
data: { unread_count: count, unread_conversations: count > 0 ? 1 : 0 },
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
return this._makeRequest('/widget/messenger/unread', {
|
|
975
|
+
method: 'GET',
|
|
976
|
+
headers: { Authorization: `Bearer ${this.sessionToken}` },
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
/**
|
|
981
|
+
* Submit conversation rating
|
|
982
|
+
* @param {string} conversationId - Conversation ID
|
|
983
|
+
* @param {Object} data - Rating data
|
|
984
|
+
* @param {number} data.rating - Rating (1-5 or thumbs up/down)
|
|
985
|
+
* @param {string} data.comment - Optional comment
|
|
986
|
+
* @returns {Promise<Object>} Response
|
|
987
|
+
*/
|
|
988
|
+
async submitRating(conversationId, data) {
|
|
989
|
+
if (!this.isSessionValid()) {
|
|
990
|
+
await this.init();
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
if (this.mock) {
|
|
994
|
+
return { status: true, message: 'Thank you for your feedback!' };
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
return this._makeRequest(`/widget/messenger/conversations/${conversationId}/rate`, {
|
|
998
|
+
method: 'POST',
|
|
999
|
+
headers: {
|
|
1000
|
+
'Content-Type': 'application/json',
|
|
1001
|
+
Authorization: `Bearer ${this.sessionToken}`,
|
|
1002
|
+
},
|
|
1003
|
+
body: JSON.stringify({
|
|
1004
|
+
rating: data.rating,
|
|
1005
|
+
comment: data.comment || '',
|
|
1006
|
+
}),
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
/**
|
|
1011
|
+
* Identify contact (for logged-in users)
|
|
1012
|
+
* @param {Object} data - Contact data
|
|
1013
|
+
* @param {string} data.email - Email address
|
|
1014
|
+
* @param {string} data.name - Name
|
|
1015
|
+
* @returns {Promise<Object>} Response
|
|
1016
|
+
*/
|
|
1017
|
+
async identifyContact(data) {
|
|
1018
|
+
if (!this.isSessionValid()) {
|
|
1019
|
+
await this.init();
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
if (this.mock) {
|
|
1023
|
+
return { status: true, message: 'Contact identified' };
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
return this._makeRequest('/widget/messenger/identify', {
|
|
1027
|
+
method: 'POST',
|
|
1028
|
+
headers: {
|
|
1029
|
+
'Content-Type': 'application/json',
|
|
1030
|
+
Authorization: `Bearer ${this.sessionToken}`,
|
|
1031
|
+
},
|
|
1032
|
+
body: JSON.stringify(data),
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
// ==========================================
|
|
1037
|
+
// HELP ARTICLES ENDPOINTS
|
|
1038
|
+
// ==========================================
|
|
1039
|
+
|
|
1040
|
+
/**
|
|
1041
|
+
* Get help collections
|
|
1042
|
+
* @param {Object} options - Query options
|
|
1043
|
+
* @returns {Promise<Object>} Collections list
|
|
1044
|
+
*/
|
|
1045
|
+
async getHelpCollections(options = {}) {
|
|
1046
|
+
if (!this.isSessionValid()) {
|
|
1047
|
+
await this.init();
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
if (this.mock) {
|
|
1051
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
1052
|
+
return { status: true, data: MOCK_HELP_COLLECTIONS };
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
const params = new URLSearchParams();
|
|
1056
|
+
if (options.limit) params.append('limit', options.limit);
|
|
1057
|
+
|
|
1058
|
+
const endpoint = `/widget/help/collections${params.toString() ? '?' + params.toString() : ''}`;
|
|
1059
|
+
return this._makeRequest(endpoint, {
|
|
1060
|
+
method: 'GET',
|
|
1061
|
+
headers: { Authorization: `Bearer ${this.sessionToken}` },
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
/**
|
|
1066
|
+
* Search help articles
|
|
1067
|
+
* @param {string} query - Search query
|
|
1068
|
+
* @param {Object} options - Query options
|
|
1069
|
+
* @returns {Promise<Object>} Search results
|
|
1070
|
+
*/
|
|
1071
|
+
async searchHelpArticles(query, options = {}) {
|
|
1072
|
+
if (!this.isSessionValid()) {
|
|
1073
|
+
await this.init();
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
if (this.mock) {
|
|
1077
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
1078
|
+
const filtered = MOCK_HELP_COLLECTIONS.filter(
|
|
1079
|
+
(c) =>
|
|
1080
|
+
c.title.toLowerCase().includes(query.toLowerCase()) ||
|
|
1081
|
+
c.description.toLowerCase().includes(query.toLowerCase())
|
|
1082
|
+
);
|
|
1083
|
+
return { status: true, data: filtered };
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
const params = new URLSearchParams({ q: query });
|
|
1087
|
+
if (options.limit) params.append('limit', options.limit);
|
|
1088
|
+
|
|
1089
|
+
return this._makeRequest(`/widget/help/search?${params.toString()}`, {
|
|
1090
|
+
method: 'GET',
|
|
1091
|
+
headers: { Authorization: `Bearer ${this.sessionToken}` },
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
// ==========================================
|
|
1096
|
+
// CHANGELOG ENDPOINTS
|
|
1097
|
+
// ==========================================
|
|
1098
|
+
|
|
563
1099
|
/**
|
|
564
1100
|
* Get published changelogs
|
|
565
1101
|
* @param {Object} options - Optional query parameters
|
|
@@ -1013,6 +1549,9 @@
|
|
|
1013
1549
|
});
|
|
1014
1550
|
|
|
1015
1551
|
class BaseWidget {
|
|
1552
|
+
static STORAGE_KEY = 'feedback_submitted';
|
|
1553
|
+
static DEFAULT_COOLDOWN_DAYS = 30; // Don't show for 30 days after submission
|
|
1554
|
+
|
|
1016
1555
|
constructor(options = {}) {
|
|
1017
1556
|
this.id = options.id;
|
|
1018
1557
|
this.sdk = options.sdk;
|
|
@@ -1031,6 +1570,8 @@
|
|
|
1031
1570
|
autoShow: false,
|
|
1032
1571
|
showBackdrop: true,
|
|
1033
1572
|
customStyles: {},
|
|
1573
|
+
suppressAfterSubmission: true, // Don't show widget again after submission
|
|
1574
|
+
suppressionDays: BaseWidget.DEFAULT_COOLDOWN_DAYS,
|
|
1034
1575
|
...options,
|
|
1035
1576
|
};
|
|
1036
1577
|
|
|
@@ -1057,6 +1598,15 @@
|
|
|
1057
1598
|
mount(container) {
|
|
1058
1599
|
if (this.mounted || this.destroyed) return this;
|
|
1059
1600
|
|
|
1601
|
+
// Check if feedback was recently submitted and should be suppressed
|
|
1602
|
+
if (this.options.suppressAfterSubmission && this._hasRecentlySubmitted()) {
|
|
1603
|
+
this.sdk.eventBus.emit('widget:suppressed', {
|
|
1604
|
+
widget: this,
|
|
1605
|
+
reason: 'recently_submitted'
|
|
1606
|
+
});
|
|
1607
|
+
return this;
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1060
1610
|
if (typeof container === 'string') {
|
|
1061
1611
|
container = document.querySelector(container);
|
|
1062
1612
|
}
|
|
@@ -1202,6 +1752,9 @@
|
|
|
1202
1752
|
|
|
1203
1753
|
const response = await this.apiService.submitFeedback(payload);
|
|
1204
1754
|
|
|
1755
|
+
// Track that feedback was submitted
|
|
1756
|
+
this._trackSubmission();
|
|
1757
|
+
|
|
1205
1758
|
this._showSuccessMessage();
|
|
1206
1759
|
this.closePanel();
|
|
1207
1760
|
|
|
@@ -1243,6 +1796,85 @@
|
|
|
1243
1796
|
onMount() {}
|
|
1244
1797
|
onDestroy() {}
|
|
1245
1798
|
|
|
1799
|
+
/**
|
|
1800
|
+
* Track that feedback was submitted to localStorage
|
|
1801
|
+
*/
|
|
1802
|
+
_trackSubmission() {
|
|
1803
|
+
try {
|
|
1804
|
+
const workspace = this.sdk.config.workspace;
|
|
1805
|
+
const storageKey = `${BaseWidget.STORAGE_KEY}_${workspace}`;
|
|
1806
|
+
const data = {
|
|
1807
|
+
submittedAt: Date.now(),
|
|
1808
|
+
boardId: this.options.boardId,
|
|
1809
|
+
};
|
|
1810
|
+
localStorage.setItem(storageKey, JSON.stringify(data));
|
|
1811
|
+
} catch (e) {
|
|
1812
|
+
// localStorage may not be available
|
|
1813
|
+
console.warn('Failed to track feedback submission:', e);
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
/**
|
|
1818
|
+
* Check if feedback was recently submitted (within cooldown period)
|
|
1819
|
+
* Uses backend tracking (preferred) with localStorage as fallback
|
|
1820
|
+
* @returns {boolean} true if feedback was submitted within the cooldown period
|
|
1821
|
+
*/
|
|
1822
|
+
_hasRecentlySubmitted() {
|
|
1823
|
+
const cooldownMs = this.options.suppressionDays * 24 * 60 * 60 * 1000;
|
|
1824
|
+
const now = Date.now();
|
|
1825
|
+
|
|
1826
|
+
// Check backend tracking first (from init response)
|
|
1827
|
+
if (this.sdk.config.last_feedback_at) {
|
|
1828
|
+
try {
|
|
1829
|
+
const backendTimestamp = new Date(this.sdk.config.last_feedback_at).getTime();
|
|
1830
|
+
if ((now - backendTimestamp) < cooldownMs) {
|
|
1831
|
+
return true;
|
|
1832
|
+
}
|
|
1833
|
+
} catch (e) {
|
|
1834
|
+
// Invalid date format, continue to localStorage check
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
// Fallback to localStorage
|
|
1839
|
+
try {
|
|
1840
|
+
const workspace = this.sdk.config.workspace;
|
|
1841
|
+
const storageKey = `${BaseWidget.STORAGE_KEY}_${workspace}`;
|
|
1842
|
+
const stored = localStorage.getItem(storageKey);
|
|
1843
|
+
|
|
1844
|
+
if (!stored) return false;
|
|
1845
|
+
|
|
1846
|
+
const data = JSON.parse(stored);
|
|
1847
|
+
const submittedAt = data.submittedAt;
|
|
1848
|
+
|
|
1849
|
+
return (now - submittedAt) < cooldownMs;
|
|
1850
|
+
} catch (e) {
|
|
1851
|
+
// localStorage may not be available or data is corrupted
|
|
1852
|
+
return false;
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
/**
|
|
1857
|
+
* Clear the submission tracking (allow showing the widget again)
|
|
1858
|
+
*/
|
|
1859
|
+
clearSubmissionTracking() {
|
|
1860
|
+
try {
|
|
1861
|
+
const workspace = this.sdk.config.workspace;
|
|
1862
|
+
const storageKey = `${BaseWidget.STORAGE_KEY}_${workspace}`;
|
|
1863
|
+
localStorage.removeItem(storageKey);
|
|
1864
|
+
} catch (e) {
|
|
1865
|
+
console.warn('Failed to clear submission tracking:', e);
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
/**
|
|
1870
|
+
* Check if the widget should be shown based on submission history
|
|
1871
|
+
* @returns {boolean} true if the widget should be shown
|
|
1872
|
+
*/
|
|
1873
|
+
shouldShow() {
|
|
1874
|
+
if (!this.options.suppressAfterSubmission) return true;
|
|
1875
|
+
return !this._hasRecentlySubmitted();
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1246
1878
|
_render() {
|
|
1247
1879
|
throw new Error('_render() must be implemented by concrete widget');
|
|
1248
1880
|
}
|
|
@@ -2372,6 +3004,18 @@
|
|
|
2372
3004
|
this.enableHelp = options.enableHelp !== false;
|
|
2373
3005
|
this.enableChangelog = options.enableChangelog !== false;
|
|
2374
3006
|
|
|
3007
|
+
// Agent availability
|
|
3008
|
+
this.agentsOnline = false;
|
|
3009
|
+
this.onlineCount = 0;
|
|
3010
|
+
this.responseTime = 'Usually replies within a few minutes';
|
|
3011
|
+
|
|
3012
|
+
// Typing indicators
|
|
3013
|
+
this.typingUsers = {}; // { conversationId: { userName, timestamp } }
|
|
3014
|
+
|
|
3015
|
+
// Loading states
|
|
3016
|
+
this.isLoading = false;
|
|
3017
|
+
this.isLoadingMessages = false;
|
|
3018
|
+
|
|
2375
3019
|
// Listeners
|
|
2376
3020
|
this._listeners = new Set();
|
|
2377
3021
|
}
|
|
@@ -3154,6 +3798,9 @@
|
|
|
3154
3798
|
this.options = options;
|
|
3155
3799
|
this.element = null;
|
|
3156
3800
|
this._unsubscribe = null;
|
|
3801
|
+
this._typingTimeout = null;
|
|
3802
|
+
this._isTyping = false;
|
|
3803
|
+
this._typingIndicator = null;
|
|
3157
3804
|
}
|
|
3158
3805
|
|
|
3159
3806
|
render() {
|
|
@@ -3168,8 +3815,21 @@
|
|
|
3168
3815
|
type === 'messageAdded' &&
|
|
3169
3816
|
data.conversationId === this.state.activeConversationId
|
|
3170
3817
|
) {
|
|
3818
|
+
this._hideTypingIndicator();
|
|
3171
3819
|
this._appendMessage(data.message);
|
|
3172
3820
|
this._scrollToBottom();
|
|
3821
|
+
} else if (
|
|
3822
|
+
type === 'typingStarted' &&
|
|
3823
|
+
data.conversationId === this.state.activeConversationId
|
|
3824
|
+
) {
|
|
3825
|
+
this._showTypingIndicator(data.userName);
|
|
3826
|
+
} else if (
|
|
3827
|
+
type === 'typingStopped' &&
|
|
3828
|
+
data.conversationId === this.state.activeConversationId
|
|
3829
|
+
) {
|
|
3830
|
+
this._hideTypingIndicator();
|
|
3831
|
+
} else if (type === 'messagesUpdate' && data.conversationId === this.state.activeConversationId) {
|
|
3832
|
+
this._updateContent();
|
|
3173
3833
|
}
|
|
3174
3834
|
});
|
|
3175
3835
|
|
|
@@ -3179,13 +3839,20 @@
|
|
|
3179
3839
|
_updateContent() {
|
|
3180
3840
|
const conversation = this.state.getActiveConversation();
|
|
3181
3841
|
const messages = this.state.getActiveMessages();
|
|
3842
|
+
const isNewConversation = !this.state.activeConversationId;
|
|
3182
3843
|
|
|
3183
3844
|
const messagesHtml =
|
|
3184
3845
|
messages.length === 0
|
|
3185
|
-
? this._renderEmptyState()
|
|
3846
|
+
? this._renderEmptyState(isNewConversation)
|
|
3186
3847
|
: messages.map((msg) => this._renderMessage(msg)).join('');
|
|
3187
3848
|
|
|
3188
3849
|
const avatarHtml = this._renderConversationAvatar(conversation);
|
|
3850
|
+
const title = isNewConversation
|
|
3851
|
+
? 'New conversation'
|
|
3852
|
+
: conversation?.title || 'Chat with team';
|
|
3853
|
+
const placeholder = isNewConversation
|
|
3854
|
+
? 'Start typing your message...'
|
|
3855
|
+
: 'Write a message...';
|
|
3189
3856
|
|
|
3190
3857
|
this.element.innerHTML = `
|
|
3191
3858
|
<div class="messenger-chat-header">
|
|
@@ -3194,7 +3861,7 @@
|
|
|
3194
3861
|
</button>
|
|
3195
3862
|
<div class="messenger-chat-header-info">
|
|
3196
3863
|
${avatarHtml}
|
|
3197
|
-
<span class="messenger-chat-title">${
|
|
3864
|
+
<span class="messenger-chat-title">${title}</span>
|
|
3198
3865
|
</div>
|
|
3199
3866
|
<button class="messenger-close-btn" aria-label="Close">
|
|
3200
3867
|
<i class="ph ph-x" style="font-size: 20px;"></i>
|
|
@@ -3203,11 +3870,17 @@
|
|
|
3203
3870
|
|
|
3204
3871
|
<div class="messenger-chat-messages">
|
|
3205
3872
|
${messagesHtml}
|
|
3873
|
+
<div class="messenger-typing-indicator" style="display: none;">
|
|
3874
|
+
<div class="messenger-typing-dots">
|
|
3875
|
+
<span></span><span></span><span></span>
|
|
3876
|
+
</div>
|
|
3877
|
+
<span class="messenger-typing-text"></span>
|
|
3878
|
+
</div>
|
|
3206
3879
|
</div>
|
|
3207
3880
|
|
|
3208
3881
|
<div class="messenger-chat-compose">
|
|
3209
3882
|
<div class="messenger-compose-input-wrapper">
|
|
3210
|
-
<textarea class="messenger-compose-input" placeholder="
|
|
3883
|
+
<textarea class="messenger-compose-input" placeholder="${placeholder}" rows="1"></textarea>
|
|
3211
3884
|
</div>
|
|
3212
3885
|
<button class="messenger-compose-send" aria-label="Send" disabled>
|
|
3213
3886
|
<i class="ph ph-paper-plane-tilt" style="font-size: 20px;"></i>
|
|
@@ -3215,17 +3888,25 @@
|
|
|
3215
3888
|
</div>
|
|
3216
3889
|
`;
|
|
3217
3890
|
|
|
3891
|
+
this._typingIndicator = this.element.querySelector('.messenger-typing-indicator');
|
|
3218
3892
|
this._attachEvents();
|
|
3219
3893
|
this._scrollToBottom();
|
|
3220
3894
|
}
|
|
3221
3895
|
|
|
3222
|
-
_renderEmptyState() {
|
|
3896
|
+
_renderEmptyState(isNewConversation = false) {
|
|
3223
3897
|
const avatarHtml = this._renderTeamAvatars();
|
|
3898
|
+
const responseTime = this.state.responseTime || 'We typically reply within a few minutes';
|
|
3899
|
+
const isOnline = this.state.agentsOnline;
|
|
3900
|
+
|
|
3224
3901
|
return `
|
|
3225
3902
|
<div class="messenger-chat-empty">
|
|
3226
3903
|
<div class="messenger-chat-empty-avatars">${avatarHtml}</div>
|
|
3227
|
-
<h3
|
|
3904
|
+
<h3>${isNewConversation ? 'Start a new conversation' : 'Start the conversation'}</h3>
|
|
3228
3905
|
<p>Send us a message and we'll get back to you as soon as possible.</p>
|
|
3906
|
+
<div class="messenger-chat-availability">
|
|
3907
|
+
<span class="messenger-availability-dot ${isOnline ? 'messenger-availability-online' : 'messenger-availability-away'}"></span>
|
|
3908
|
+
<span>${isOnline ? "We're online now" : responseTime}</span>
|
|
3909
|
+
</div>
|
|
3229
3910
|
</div>
|
|
3230
3911
|
`;
|
|
3231
3912
|
}
|
|
@@ -3379,6 +4060,11 @@
|
|
|
3379
4060
|
|
|
3380
4061
|
// Enable/disable send button
|
|
3381
4062
|
sendBtn.disabled = !input.value.trim();
|
|
4063
|
+
|
|
4064
|
+
// Send typing indicator
|
|
4065
|
+
if (input.value.trim()) {
|
|
4066
|
+
this._startTyping();
|
|
4067
|
+
}
|
|
3382
4068
|
});
|
|
3383
4069
|
|
|
3384
4070
|
input.addEventListener('keydown', (e) => {
|
|
@@ -3399,24 +4085,83 @@
|
|
|
3399
4085
|
|
|
3400
4086
|
if (!content) return;
|
|
3401
4087
|
|
|
3402
|
-
//
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
isOwn: true,
|
|
3407
|
-
timestamp: new Date().toISOString(),
|
|
3408
|
-
};
|
|
4088
|
+
// Stop typing indicator
|
|
4089
|
+
this._stopTyping();
|
|
4090
|
+
|
|
4091
|
+
const isNewConversation = !this.state.activeConversationId;
|
|
3409
4092
|
|
|
3410
|
-
|
|
4093
|
+
if (isNewConversation) {
|
|
4094
|
+
// Start a new conversation
|
|
4095
|
+
if (this.options.onStartConversation) {
|
|
4096
|
+
this.options.onStartConversation(content);
|
|
4097
|
+
}
|
|
4098
|
+
} else {
|
|
4099
|
+
// Add message to existing conversation
|
|
4100
|
+
const message = {
|
|
4101
|
+
id: 'msg_' + Date.now(),
|
|
4102
|
+
content: content,
|
|
4103
|
+
isOwn: true,
|
|
4104
|
+
timestamp: new Date().toISOString(),
|
|
4105
|
+
};
|
|
4106
|
+
|
|
4107
|
+
this.state.addMessage(this.state.activeConversationId, message);
|
|
4108
|
+
|
|
4109
|
+
// Emit event for API integration
|
|
4110
|
+
if (this.options.onSendMessage) {
|
|
4111
|
+
this.options.onSendMessage(this.state.activeConversationId, message);
|
|
4112
|
+
}
|
|
4113
|
+
}
|
|
3411
4114
|
|
|
3412
4115
|
// Clear input
|
|
3413
4116
|
input.value = '';
|
|
3414
4117
|
input.style.height = 'auto';
|
|
3415
4118
|
this.element.querySelector('.messenger-compose-send').disabled = true;
|
|
4119
|
+
}
|
|
4120
|
+
|
|
4121
|
+
_startTyping() {
|
|
4122
|
+
if (!this._isTyping && this.state.activeConversationId) {
|
|
4123
|
+
this._isTyping = true;
|
|
4124
|
+
if (this.options.onTyping) {
|
|
4125
|
+
this.options.onTyping(this.state.activeConversationId, true);
|
|
4126
|
+
}
|
|
4127
|
+
}
|
|
4128
|
+
|
|
4129
|
+
// Reset typing timeout
|
|
4130
|
+
if (this._typingTimeout) {
|
|
4131
|
+
clearTimeout(this._typingTimeout);
|
|
4132
|
+
}
|
|
4133
|
+
this._typingTimeout = setTimeout(() => {
|
|
4134
|
+
this._stopTyping();
|
|
4135
|
+
}, 3000);
|
|
4136
|
+
}
|
|
4137
|
+
|
|
4138
|
+
_stopTyping() {
|
|
4139
|
+
if (this._isTyping && this.state.activeConversationId) {
|
|
4140
|
+
this._isTyping = false;
|
|
4141
|
+
if (this.options.onTyping) {
|
|
4142
|
+
this.options.onTyping(this.state.activeConversationId, false);
|
|
4143
|
+
}
|
|
4144
|
+
}
|
|
4145
|
+
if (this._typingTimeout) {
|
|
4146
|
+
clearTimeout(this._typingTimeout);
|
|
4147
|
+
this._typingTimeout = null;
|
|
4148
|
+
}
|
|
4149
|
+
}
|
|
4150
|
+
|
|
4151
|
+
_showTypingIndicator(userName) {
|
|
4152
|
+
if (this._typingIndicator) {
|
|
4153
|
+
this._typingIndicator.style.display = 'flex';
|
|
4154
|
+
const textEl = this._typingIndicator.querySelector('.messenger-typing-text');
|
|
4155
|
+
if (textEl) {
|
|
4156
|
+
textEl.textContent = `${userName || 'Support'} is typing...`;
|
|
4157
|
+
}
|
|
4158
|
+
this._scrollToBottom();
|
|
4159
|
+
}
|
|
4160
|
+
}
|
|
3416
4161
|
|
|
3417
|
-
|
|
3418
|
-
if (this.
|
|
3419
|
-
this.
|
|
4162
|
+
_hideTypingIndicator() {
|
|
4163
|
+
if (this._typingIndicator) {
|
|
4164
|
+
this._typingIndicator.style.display = 'none';
|
|
3420
4165
|
}
|
|
3421
4166
|
}
|
|
3422
4167
|
|
|
@@ -3424,6 +4169,10 @@
|
|
|
3424
4169
|
if (this._unsubscribe) {
|
|
3425
4170
|
this._unsubscribe();
|
|
3426
4171
|
}
|
|
4172
|
+
if (this._typingTimeout) {
|
|
4173
|
+
clearTimeout(this._typingTimeout);
|
|
4174
|
+
}
|
|
4175
|
+
this._stopTyping();
|
|
3427
4176
|
if (this.element && this.element.parentNode) {
|
|
3428
4177
|
this.element.parentNode.removeChild(this.element);
|
|
3429
4178
|
}
|
|
@@ -3617,6 +4366,11 @@
|
|
|
3617
4366
|
this.state.setActiveConversation(convId);
|
|
3618
4367
|
this.state.markAsRead(convId);
|
|
3619
4368
|
this.state.setView('chat');
|
|
4369
|
+
|
|
4370
|
+
// Notify widget to fetch messages
|
|
4371
|
+
if (this.options.onSelectConversation) {
|
|
4372
|
+
this.options.onSelectConversation(convId);
|
|
4373
|
+
}
|
|
3620
4374
|
});
|
|
3621
4375
|
});
|
|
3622
4376
|
|
|
@@ -3630,18 +4384,14 @@
|
|
|
3630
4384
|
}
|
|
3631
4385
|
|
|
3632
4386
|
_startNewConversation() {
|
|
3633
|
-
//
|
|
3634
|
-
|
|
3635
|
-
id: 'conv_' + Date.now(),
|
|
3636
|
-
title: 'New conversation',
|
|
3637
|
-
participants: [],
|
|
3638
|
-
lastMessage: null,
|
|
3639
|
-
lastMessageTime: new Date().toISOString(),
|
|
3640
|
-
unread: 0,
|
|
3641
|
-
};
|
|
3642
|
-
this.state.addConversation(newConv);
|
|
3643
|
-
this.state.setActiveConversation(newConv.id);
|
|
4387
|
+
// Set view to chat with no active conversation (new conversation mode)
|
|
4388
|
+
this.state.setActiveConversation(null);
|
|
3644
4389
|
this.state.setView('chat');
|
|
4390
|
+
|
|
4391
|
+
// Notify widget to handle new conversation flow
|
|
4392
|
+
if (this.options.onStartNewConversation) {
|
|
4393
|
+
this.options.onStartNewConversation();
|
|
4394
|
+
}
|
|
3645
4395
|
}
|
|
3646
4396
|
|
|
3647
4397
|
destroy() {
|
|
@@ -3865,7 +4615,7 @@
|
|
|
3865
4615
|
|
|
3866
4616
|
// Subscribe to state changes to re-render when data loads
|
|
3867
4617
|
this._unsubscribe = this.state.subscribe((type) => {
|
|
3868
|
-
if (type === 'homeChangelogUpdate' || type === 'conversationsUpdate') {
|
|
4618
|
+
if (type === 'homeChangelogUpdate' || type === 'conversationsUpdate' || type === 'availabilityUpdate') {
|
|
3869
4619
|
this._updateContent();
|
|
3870
4620
|
}
|
|
3871
4621
|
});
|
|
@@ -3891,6 +4641,7 @@
|
|
|
3891
4641
|
<div class="messenger-home-welcome">
|
|
3892
4642
|
<span class="messenger-home-greeting">Hello there.</span>
|
|
3893
4643
|
<span class="messenger-home-question">${this.state.welcomeMessage}</span>
|
|
4644
|
+
${this._renderAvailabilityStatus()}
|
|
3894
4645
|
</div>
|
|
3895
4646
|
</div>
|
|
3896
4647
|
|
|
@@ -3939,6 +4690,27 @@
|
|
|
3939
4690
|
return colors[index % colors.length];
|
|
3940
4691
|
}
|
|
3941
4692
|
|
|
4693
|
+
_renderAvailabilityStatus() {
|
|
4694
|
+
const isOnline = this.state.agentsOnline;
|
|
4695
|
+
const responseTime = this.state.responseTime || 'We typically reply within a few minutes';
|
|
4696
|
+
|
|
4697
|
+
if (isOnline) {
|
|
4698
|
+
return `
|
|
4699
|
+
<div class="messenger-home-availability">
|
|
4700
|
+
<span class="messenger-availability-dot messenger-availability-online"></span>
|
|
4701
|
+
<span class="messenger-availability-text">We're online now</span>
|
|
4702
|
+
</div>
|
|
4703
|
+
`;
|
|
4704
|
+
}
|
|
4705
|
+
|
|
4706
|
+
return `
|
|
4707
|
+
<div class="messenger-home-availability">
|
|
4708
|
+
<span class="messenger-availability-dot messenger-availability-away"></span>
|
|
4709
|
+
<span class="messenger-availability-text">${responseTime}</span>
|
|
4710
|
+
</div>
|
|
4711
|
+
`;
|
|
4712
|
+
}
|
|
4713
|
+
|
|
3942
4714
|
_renderFeaturedCard() {
|
|
3943
4715
|
// Only show if there's featured content configured
|
|
3944
4716
|
if (!this.options.featuredContent) {
|
|
@@ -4061,12 +4833,286 @@
|
|
|
4061
4833
|
}
|
|
4062
4834
|
}
|
|
4063
4835
|
|
|
4064
|
-
destroy() {
|
|
4065
|
-
if (this._unsubscribe) {
|
|
4066
|
-
this._unsubscribe();
|
|
4067
|
-
}
|
|
4068
|
-
if (this.element && this.element.parentNode) {
|
|
4069
|
-
this.element.parentNode.removeChild(this.element);
|
|
4836
|
+
destroy() {
|
|
4837
|
+
if (this._unsubscribe) {
|
|
4838
|
+
this._unsubscribe();
|
|
4839
|
+
}
|
|
4840
|
+
if (this.element && this.element.parentNode) {
|
|
4841
|
+
this.element.parentNode.removeChild(this.element);
|
|
4842
|
+
}
|
|
4843
|
+
}
|
|
4844
|
+
}
|
|
4845
|
+
|
|
4846
|
+
/**
|
|
4847
|
+
* WebSocketService - Real-time communication for messenger widget
|
|
4848
|
+
*/
|
|
4849
|
+
|
|
4850
|
+
class WebSocketService {
|
|
4851
|
+
constructor(config = {}) {
|
|
4852
|
+
this.baseURL = config.baseURL || '';
|
|
4853
|
+
this.workspace = config.workspace || '';
|
|
4854
|
+
this.sessionToken = config.sessionToken || null;
|
|
4855
|
+
this.mock = config.mock || false;
|
|
4856
|
+
|
|
4857
|
+
this.ws = null;
|
|
4858
|
+
this.reconnectAttempts = 0;
|
|
4859
|
+
this.maxReconnectAttempts = 5;
|
|
4860
|
+
this.reconnectDelay = 1000;
|
|
4861
|
+
this.pingInterval = null;
|
|
4862
|
+
this.isConnected = false;
|
|
4863
|
+
|
|
4864
|
+
// Event listeners
|
|
4865
|
+
this._listeners = new Map();
|
|
4866
|
+
|
|
4867
|
+
// Bind methods
|
|
4868
|
+
this._onOpen = this._onOpen.bind(this);
|
|
4869
|
+
this._onMessage = this._onMessage.bind(this);
|
|
4870
|
+
this._onClose = this._onClose.bind(this);
|
|
4871
|
+
this._onError = this._onError.bind(this);
|
|
4872
|
+
}
|
|
4873
|
+
|
|
4874
|
+
/**
|
|
4875
|
+
* Connect to WebSocket server
|
|
4876
|
+
*/
|
|
4877
|
+
connect(sessionToken = null) {
|
|
4878
|
+
if (sessionToken) {
|
|
4879
|
+
this.sessionToken = sessionToken;
|
|
4880
|
+
}
|
|
4881
|
+
|
|
4882
|
+
if (!this.sessionToken) {
|
|
4883
|
+
console.warn('[WebSocket] No session token provided');
|
|
4884
|
+
return;
|
|
4885
|
+
}
|
|
4886
|
+
|
|
4887
|
+
// Mock mode - simulate connection
|
|
4888
|
+
if (this.mock) {
|
|
4889
|
+
this.isConnected = true;
|
|
4890
|
+
this._emit('connected', {});
|
|
4891
|
+
this._startMockResponses();
|
|
4892
|
+
return;
|
|
4893
|
+
}
|
|
4894
|
+
|
|
4895
|
+
// Build WebSocket URL
|
|
4896
|
+
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
4897
|
+
let wsURL = this.baseURL.replace(/^https?:/, wsProtocol);
|
|
4898
|
+
wsURL = wsURL.replace('/api/v1', '');
|
|
4899
|
+
wsURL = `${wsURL}/api/v1/widget/messenger/ws?token=${encodeURIComponent(this.sessionToken)}`;
|
|
4900
|
+
|
|
4901
|
+
try {
|
|
4902
|
+
this.ws = new WebSocket(wsURL);
|
|
4903
|
+
this.ws.onopen = this._onOpen;
|
|
4904
|
+
this.ws.onmessage = this._onMessage;
|
|
4905
|
+
this.ws.onclose = this._onClose;
|
|
4906
|
+
this.ws.onerror = this._onError;
|
|
4907
|
+
} catch (error) {
|
|
4908
|
+
console.error('[WebSocket] Connection error:', error);
|
|
4909
|
+
this._scheduleReconnect();
|
|
4910
|
+
}
|
|
4911
|
+
}
|
|
4912
|
+
|
|
4913
|
+
/**
|
|
4914
|
+
* Disconnect from WebSocket server
|
|
4915
|
+
*/
|
|
4916
|
+
disconnect() {
|
|
4917
|
+
this.isConnected = false;
|
|
4918
|
+
this.reconnectAttempts = this.maxReconnectAttempts; // Prevent reconnection
|
|
4919
|
+
|
|
4920
|
+
if (this.pingInterval) {
|
|
4921
|
+
clearInterval(this.pingInterval);
|
|
4922
|
+
this.pingInterval = null;
|
|
4923
|
+
}
|
|
4924
|
+
|
|
4925
|
+
if (this.ws) {
|
|
4926
|
+
this.ws.close();
|
|
4927
|
+
this.ws = null;
|
|
4928
|
+
}
|
|
4929
|
+
|
|
4930
|
+
if (this._mockInterval) {
|
|
4931
|
+
clearInterval(this._mockInterval);
|
|
4932
|
+
this._mockInterval = null;
|
|
4933
|
+
}
|
|
4934
|
+
}
|
|
4935
|
+
|
|
4936
|
+
/**
|
|
4937
|
+
* Subscribe to events
|
|
4938
|
+
* @param {string} event - Event name
|
|
4939
|
+
* @param {Function} callback - Event handler
|
|
4940
|
+
* @returns {Function} Unsubscribe function
|
|
4941
|
+
*/
|
|
4942
|
+
on(event, callback) {
|
|
4943
|
+
if (!this._listeners.has(event)) {
|
|
4944
|
+
this._listeners.set(event, new Set());
|
|
4945
|
+
}
|
|
4946
|
+
this._listeners.get(event).add(callback);
|
|
4947
|
+
return () => this._listeners.get(event).delete(callback);
|
|
4948
|
+
}
|
|
4949
|
+
|
|
4950
|
+
/**
|
|
4951
|
+
* Remove event listener
|
|
4952
|
+
*/
|
|
4953
|
+
off(event, callback) {
|
|
4954
|
+
if (this._listeners.has(event)) {
|
|
4955
|
+
this._listeners.get(event).delete(callback);
|
|
4956
|
+
}
|
|
4957
|
+
}
|
|
4958
|
+
|
|
4959
|
+
/**
|
|
4960
|
+
* Send message through WebSocket
|
|
4961
|
+
*/
|
|
4962
|
+
send(type, payload = {}) {
|
|
4963
|
+
if (!this.isConnected) {
|
|
4964
|
+
console.warn('[WebSocket] Not connected, cannot send message');
|
|
4965
|
+
return;
|
|
4966
|
+
}
|
|
4967
|
+
|
|
4968
|
+
if (this.mock) {
|
|
4969
|
+
// Mock mode - just log
|
|
4970
|
+
console.log('[WebSocket Mock] Sending:', type, payload);
|
|
4971
|
+
return;
|
|
4972
|
+
}
|
|
4973
|
+
|
|
4974
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
4975
|
+
this.ws.send(JSON.stringify({ type, payload }));
|
|
4976
|
+
}
|
|
4977
|
+
}
|
|
4978
|
+
|
|
4979
|
+
// Private methods
|
|
4980
|
+
|
|
4981
|
+
_onOpen() {
|
|
4982
|
+
console.log('[WebSocket] Connected');
|
|
4983
|
+
this.isConnected = true;
|
|
4984
|
+
this.reconnectAttempts = 0;
|
|
4985
|
+
this._emit('connected', {});
|
|
4986
|
+
|
|
4987
|
+
// Start ping interval to keep connection alive
|
|
4988
|
+
this.pingInterval = setInterval(() => {
|
|
4989
|
+
this.send('ping', {});
|
|
4990
|
+
}, 30000);
|
|
4991
|
+
}
|
|
4992
|
+
|
|
4993
|
+
_onMessage(event) {
|
|
4994
|
+
try {
|
|
4995
|
+
const data = JSON.parse(event.data);
|
|
4996
|
+
const { type, payload } = data;
|
|
4997
|
+
|
|
4998
|
+
// Handle different event types
|
|
4999
|
+
switch (type) {
|
|
5000
|
+
case 'message:new':
|
|
5001
|
+
this._emit('message', payload);
|
|
5002
|
+
break;
|
|
5003
|
+
case 'typing:started':
|
|
5004
|
+
this._emit('typing_started', payload);
|
|
5005
|
+
break;
|
|
5006
|
+
case 'typing:stopped':
|
|
5007
|
+
this._emit('typing_stopped', payload);
|
|
5008
|
+
break;
|
|
5009
|
+
case 'conversation:updated':
|
|
5010
|
+
this._emit('conversation_updated', payload);
|
|
5011
|
+
break;
|
|
5012
|
+
case 'conversation:closed':
|
|
5013
|
+
this._emit('conversation_closed', payload);
|
|
5014
|
+
break;
|
|
5015
|
+
case 'availability:changed':
|
|
5016
|
+
this._emit('availability_changed', payload);
|
|
5017
|
+
break;
|
|
5018
|
+
case 'pong':
|
|
5019
|
+
// Ping response, ignore
|
|
5020
|
+
break;
|
|
5021
|
+
default:
|
|
5022
|
+
console.log('[WebSocket] Unknown event:', type, payload);
|
|
5023
|
+
}
|
|
5024
|
+
} catch (error) {
|
|
5025
|
+
console.error('[WebSocket] Failed to parse message:', error);
|
|
5026
|
+
}
|
|
5027
|
+
}
|
|
5028
|
+
|
|
5029
|
+
_onClose(event) {
|
|
5030
|
+
console.log('[WebSocket] Disconnected:', event.code, event.reason);
|
|
5031
|
+
this.isConnected = false;
|
|
5032
|
+
|
|
5033
|
+
if (this.pingInterval) {
|
|
5034
|
+
clearInterval(this.pingInterval);
|
|
5035
|
+
this.pingInterval = null;
|
|
5036
|
+
}
|
|
5037
|
+
|
|
5038
|
+
this._emit('disconnected', { code: event.code, reason: event.reason });
|
|
5039
|
+
this._scheduleReconnect();
|
|
5040
|
+
}
|
|
5041
|
+
|
|
5042
|
+
_onError(error) {
|
|
5043
|
+
console.error('[WebSocket] Error:', error);
|
|
5044
|
+
this._emit('error', { error });
|
|
5045
|
+
}
|
|
5046
|
+
|
|
5047
|
+
_scheduleReconnect() {
|
|
5048
|
+
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
5049
|
+
console.log('[WebSocket] Max reconnect attempts reached');
|
|
5050
|
+
this._emit('reconnect_failed', {});
|
|
5051
|
+
return;
|
|
5052
|
+
}
|
|
5053
|
+
|
|
5054
|
+
this.reconnectAttempts++;
|
|
5055
|
+
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
|
|
5056
|
+
console.log(`[WebSocket] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
|
|
5057
|
+
|
|
5058
|
+
setTimeout(() => {
|
|
5059
|
+
this.connect();
|
|
5060
|
+
}, delay);
|
|
5061
|
+
}
|
|
5062
|
+
|
|
5063
|
+
_emit(event, data) {
|
|
5064
|
+
if (this._listeners.has(event)) {
|
|
5065
|
+
this._listeners.get(event).forEach((callback) => {
|
|
5066
|
+
try {
|
|
5067
|
+
callback(data);
|
|
5068
|
+
} catch (error) {
|
|
5069
|
+
console.error(`[WebSocket] Error in ${event} handler:`, error);
|
|
5070
|
+
}
|
|
5071
|
+
});
|
|
5072
|
+
}
|
|
5073
|
+
}
|
|
5074
|
+
|
|
5075
|
+
// Mock support for development
|
|
5076
|
+
_startMockResponses() {
|
|
5077
|
+
// Simulate agent typing and responses
|
|
5078
|
+
this._mockInterval = setInterval(() => {
|
|
5079
|
+
// Randomly emit typing or message events for demo
|
|
5080
|
+
const random = Math.random();
|
|
5081
|
+
if (random < 0.1) {
|
|
5082
|
+
this._emit('typing_started', {
|
|
5083
|
+
conversation_id: 'conv_1',
|
|
5084
|
+
user_id: 'agent_1',
|
|
5085
|
+
user_name: 'Sarah',
|
|
5086
|
+
is_agent: true,
|
|
5087
|
+
});
|
|
5088
|
+
|
|
5089
|
+
// Stop typing after 2 seconds
|
|
5090
|
+
setTimeout(() => {
|
|
5091
|
+
this._emit('typing_stopped', {
|
|
5092
|
+
conversation_id: 'conv_1',
|
|
5093
|
+
user_id: 'agent_1',
|
|
5094
|
+
});
|
|
5095
|
+
}, 2000);
|
|
5096
|
+
}
|
|
5097
|
+
}, 10000);
|
|
5098
|
+
}
|
|
5099
|
+
|
|
5100
|
+
/**
|
|
5101
|
+
* Simulate receiving a message (for mock mode)
|
|
5102
|
+
*/
|
|
5103
|
+
simulateMessage(conversationId, message) {
|
|
5104
|
+
if (this.mock) {
|
|
5105
|
+
this._emit('message', {
|
|
5106
|
+
conversation_id: conversationId,
|
|
5107
|
+
message: {
|
|
5108
|
+
id: 'msg_' + Date.now(),
|
|
5109
|
+
content: message.content,
|
|
5110
|
+
sender_type: message.sender_type || 'agent',
|
|
5111
|
+
sender_name: message.sender_name || 'Support',
|
|
5112
|
+
sender_avatar: message.sender_avatar || null,
|
|
5113
|
+
created_at: new Date().toISOString(),
|
|
5114
|
+
},
|
|
5115
|
+
});
|
|
4070
5116
|
}
|
|
4071
5117
|
}
|
|
4072
5118
|
}
|
|
@@ -4108,9 +5154,14 @@
|
|
|
4108
5154
|
|
|
4109
5155
|
this.launcher = null;
|
|
4110
5156
|
this.panel = null;
|
|
5157
|
+
this.wsService = null;
|
|
5158
|
+
this._wsUnsubscribers = [];
|
|
4111
5159
|
|
|
4112
5160
|
// Bind methods
|
|
4113
5161
|
this._handleOpenChange = this._handleOpenChange.bind(this);
|
|
5162
|
+
this._handleWebSocketMessage = this._handleWebSocketMessage.bind(this);
|
|
5163
|
+
this._handleTypingStarted = this._handleTypingStarted.bind(this);
|
|
5164
|
+
this._handleTypingStopped = this._handleTypingStopped.bind(this);
|
|
4114
5165
|
}
|
|
4115
5166
|
|
|
4116
5167
|
_render() {
|
|
@@ -4126,16 +5177,23 @@
|
|
|
4126
5177
|
});
|
|
4127
5178
|
container.appendChild(this.launcher.render());
|
|
4128
5179
|
|
|
4129
|
-
// Create panel
|
|
5180
|
+
// Create panel with all callbacks
|
|
4130
5181
|
this.panel = new MessengerPanel(this.messengerState, {
|
|
4131
5182
|
position: this.messengerOptions.position,
|
|
4132
5183
|
theme: this.messengerOptions.theme,
|
|
4133
5184
|
primaryColor: this.messengerOptions.primaryColor,
|
|
4134
5185
|
logoUrl: this.messengerOptions.logoUrl,
|
|
4135
5186
|
featuredContent: this.messengerOptions.featuredContent,
|
|
5187
|
+
// Chat callbacks
|
|
4136
5188
|
onSendMessage:
|
|
4137
5189
|
this.messengerOptions.onSendMessage ||
|
|
4138
5190
|
this._handleSendMessage.bind(this),
|
|
5191
|
+
onStartConversation: this._handleStartConversation.bind(this),
|
|
5192
|
+
onTyping: this.sendTypingIndicator.bind(this),
|
|
5193
|
+
// Conversation list callbacks
|
|
5194
|
+
onSelectConversation: this._handleSelectConversation.bind(this),
|
|
5195
|
+
onStartNewConversation: this._handleNewConversationClick.bind(this),
|
|
5196
|
+
// Article/changelog callbacks
|
|
4139
5197
|
onArticleClick: this.messengerOptions.onArticleClick,
|
|
4140
5198
|
onChangelogClick: this.messengerOptions.onChangelogClick,
|
|
4141
5199
|
});
|
|
@@ -4173,30 +5231,179 @@
|
|
|
4173
5231
|
}
|
|
4174
5232
|
}
|
|
4175
5233
|
|
|
5234
|
+
/**
|
|
5235
|
+
* Handle starting a new conversation
|
|
5236
|
+
*/
|
|
5237
|
+
async _handleStartConversation(messageContent) {
|
|
5238
|
+
try {
|
|
5239
|
+
await this.startNewConversation(messageContent);
|
|
5240
|
+
} catch (error) {
|
|
5241
|
+
console.error('[MessengerWidget] Failed to start conversation:', error);
|
|
5242
|
+
}
|
|
5243
|
+
}
|
|
5244
|
+
|
|
5245
|
+
/**
|
|
5246
|
+
* Handle selecting a conversation from the list
|
|
5247
|
+
*/
|
|
5248
|
+
async _handleSelectConversation(conversationId) {
|
|
5249
|
+
try {
|
|
5250
|
+
await this.fetchMessages(conversationId);
|
|
5251
|
+
} catch (error) {
|
|
5252
|
+
console.error('[MessengerWidget] Failed to fetch messages:', error);
|
|
5253
|
+
}
|
|
5254
|
+
}
|
|
5255
|
+
|
|
5256
|
+
/**
|
|
5257
|
+
* Handle clicking "new conversation" button
|
|
5258
|
+
*/
|
|
5259
|
+
_handleNewConversationClick() {
|
|
5260
|
+
// View is already changed by ConversationsView
|
|
5261
|
+
// This is for any additional setup needed
|
|
5262
|
+
}
|
|
5263
|
+
|
|
4176
5264
|
async _handleSendMessage(conversationId, message) {
|
|
4177
|
-
//
|
|
5265
|
+
// Emit event for external listeners
|
|
4178
5266
|
this.sdk.eventBus.emit('messenger:messageSent', {
|
|
4179
5267
|
widget: this,
|
|
4180
5268
|
conversationId,
|
|
4181
5269
|
message,
|
|
4182
5270
|
});
|
|
4183
5271
|
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
5272
|
+
try {
|
|
5273
|
+
// Send message through API
|
|
5274
|
+
const response = await this.apiService.sendMessage(conversationId, {
|
|
5275
|
+
content: message.content,
|
|
5276
|
+
});
|
|
5277
|
+
|
|
5278
|
+
if (response.status && response.data) {
|
|
5279
|
+
// Update the message ID with server-assigned ID
|
|
5280
|
+
// Message is already added to state optimistically in ChatView
|
|
5281
|
+
console.log('[MessengerWidget] Message sent:', response.data.id);
|
|
5282
|
+
}
|
|
5283
|
+
|
|
5284
|
+
// In mock mode, simulate an agent response after a delay
|
|
5285
|
+
if (this.apiService?.mock) {
|
|
5286
|
+
setTimeout(() => {
|
|
5287
|
+
const mockResponse = {
|
|
5288
|
+
id: 'msg_' + Date.now(),
|
|
5289
|
+
content: "Thanks for your message! We'll get back to you soon.",
|
|
5290
|
+
isOwn: false,
|
|
5291
|
+
timestamp: new Date().toISOString(),
|
|
5292
|
+
sender: {
|
|
5293
|
+
name: 'Support Team',
|
|
5294
|
+
avatarUrl: null,
|
|
5295
|
+
},
|
|
5296
|
+
};
|
|
5297
|
+
this.messengerState.addMessage(conversationId, mockResponse);
|
|
5298
|
+
}, 1500);
|
|
5299
|
+
}
|
|
5300
|
+
} catch (error) {
|
|
5301
|
+
console.error('[MessengerWidget] Failed to send message:', error);
|
|
5302
|
+
// Could add error handling UI here
|
|
5303
|
+
}
|
|
5304
|
+
}
|
|
5305
|
+
|
|
5306
|
+
/**
|
|
5307
|
+
* Handle incoming WebSocket message
|
|
5308
|
+
*/
|
|
5309
|
+
_handleWebSocketMessage(data) {
|
|
5310
|
+
const { conversation_id, message } = data;
|
|
5311
|
+
|
|
5312
|
+
// Transform message to local format
|
|
5313
|
+
const localMessage = {
|
|
5314
|
+
id: message.id,
|
|
5315
|
+
content: message.content,
|
|
5316
|
+
isOwn: message.sender_type === 'customer',
|
|
5317
|
+
timestamp: message.created_at,
|
|
5318
|
+
sender: {
|
|
5319
|
+
name: message.sender_name || 'Support',
|
|
5320
|
+
avatarUrl: message.sender_avatar || null,
|
|
5321
|
+
},
|
|
5322
|
+
};
|
|
5323
|
+
|
|
5324
|
+
// Add message to state
|
|
5325
|
+
this.messengerState.addMessage(conversation_id, localMessage);
|
|
5326
|
+
|
|
5327
|
+
// Update unread count if panel is closed or viewing different conversation
|
|
5328
|
+
if (!this.messengerState.isOpen || this.messengerState.activeConversationId !== conversation_id) {
|
|
5329
|
+
this._updateUnreadCount();
|
|
5330
|
+
}
|
|
5331
|
+
}
|
|
5332
|
+
|
|
5333
|
+
/**
|
|
5334
|
+
* Handle typing started event
|
|
5335
|
+
*/
|
|
5336
|
+
_handleTypingStarted(data) {
|
|
5337
|
+
if (data.is_agent) {
|
|
5338
|
+
this.messengerState._notify('typingStarted', {
|
|
5339
|
+
conversationId: data.conversation_id,
|
|
5340
|
+
userName: data.user_name,
|
|
5341
|
+
});
|
|
5342
|
+
}
|
|
5343
|
+
}
|
|
5344
|
+
|
|
5345
|
+
/**
|
|
5346
|
+
* Handle typing stopped event
|
|
5347
|
+
*/
|
|
5348
|
+
_handleTypingStopped(data) {
|
|
5349
|
+
this.messengerState._notify('typingStopped', {
|
|
5350
|
+
conversationId: data.conversation_id,
|
|
5351
|
+
});
|
|
5352
|
+
}
|
|
5353
|
+
|
|
5354
|
+
/**
|
|
5355
|
+
* Update unread count from API
|
|
5356
|
+
*/
|
|
5357
|
+
async _updateUnreadCount() {
|
|
5358
|
+
try {
|
|
5359
|
+
const response = await this.apiService.getUnreadCount();
|
|
5360
|
+
if (response.status && response.data) {
|
|
5361
|
+
this.messengerState.unreadCount = response.data.unread_count || 0;
|
|
5362
|
+
this.messengerState._notify('unreadCountChange', { count: this.messengerState.unreadCount });
|
|
5363
|
+
}
|
|
5364
|
+
} catch (error) {
|
|
5365
|
+
console.error('[MessengerWidget] Failed to get unread count:', error);
|
|
5366
|
+
}
|
|
5367
|
+
}
|
|
5368
|
+
|
|
5369
|
+
/**
|
|
5370
|
+
* Initialize WebSocket connection
|
|
5371
|
+
*/
|
|
5372
|
+
_initWebSocket() {
|
|
5373
|
+
if (this.wsService) {
|
|
5374
|
+
this.wsService.disconnect();
|
|
4199
5375
|
}
|
|
5376
|
+
|
|
5377
|
+
this.wsService = new WebSocketService({
|
|
5378
|
+
baseURL: this.apiService.baseURL,
|
|
5379
|
+
workspace: this.apiService.workspace,
|
|
5380
|
+
sessionToken: this.apiService.sessionToken,
|
|
5381
|
+
mock: this.apiService.mock,
|
|
5382
|
+
});
|
|
5383
|
+
|
|
5384
|
+
// Subscribe to WebSocket events
|
|
5385
|
+
this._wsUnsubscribers.push(
|
|
5386
|
+
this.wsService.on('message', this._handleWebSocketMessage)
|
|
5387
|
+
);
|
|
5388
|
+
this._wsUnsubscribers.push(
|
|
5389
|
+
this.wsService.on('typing_started', this._handleTypingStarted)
|
|
5390
|
+
);
|
|
5391
|
+
this._wsUnsubscribers.push(
|
|
5392
|
+
this.wsService.on('typing_stopped', this._handleTypingStopped)
|
|
5393
|
+
);
|
|
5394
|
+
this._wsUnsubscribers.push(
|
|
5395
|
+
this.wsService.on('connected', () => {
|
|
5396
|
+
console.log('[MessengerWidget] WebSocket connected');
|
|
5397
|
+
})
|
|
5398
|
+
);
|
|
5399
|
+
this._wsUnsubscribers.push(
|
|
5400
|
+
this.wsService.on('disconnected', () => {
|
|
5401
|
+
console.log('[MessengerWidget] WebSocket disconnected');
|
|
5402
|
+
})
|
|
5403
|
+
);
|
|
5404
|
+
|
|
5405
|
+
// Connect
|
|
5406
|
+
this.wsService.connect();
|
|
4200
5407
|
}
|
|
4201
5408
|
|
|
4202
5409
|
/**
|
|
@@ -4320,82 +5527,159 @@
|
|
|
4320
5527
|
}
|
|
4321
5528
|
|
|
4322
5529
|
async _fetchConversations() {
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
},
|
|
4344
|
-
];
|
|
5530
|
+
try {
|
|
5531
|
+
const response = await this.apiService.getConversations();
|
|
5532
|
+
if (response.status && response.data) {
|
|
5533
|
+
// Transform API response to local format
|
|
5534
|
+
return response.data.map((conv) => ({
|
|
5535
|
+
id: conv.id,
|
|
5536
|
+
title: conv.subject || `Chat with ${conv.assigned_user?.name || 'Support'}`,
|
|
5537
|
+
participants: conv.assigned_user
|
|
5538
|
+
? [{ name: conv.assigned_user.name, avatarUrl: conv.assigned_user.avatar }]
|
|
5539
|
+
: [{ name: 'Support', avatarUrl: null }],
|
|
5540
|
+
lastMessage: conv.preview || conv.snippet || '',
|
|
5541
|
+
lastMessageTime: conv.last_message_at,
|
|
5542
|
+
unread: conv.unread || 0,
|
|
5543
|
+
status: conv.status,
|
|
5544
|
+
}));
|
|
5545
|
+
}
|
|
5546
|
+
return [];
|
|
5547
|
+
} catch (error) {
|
|
5548
|
+
console.error('[MessengerWidget] Failed to fetch conversations:', error);
|
|
5549
|
+
return [];
|
|
4345
5550
|
}
|
|
4346
|
-
|
|
4347
|
-
// TODO: Implement API call
|
|
4348
|
-
return [];
|
|
4349
5551
|
}
|
|
4350
5552
|
|
|
4351
5553
|
async _fetchHelpArticles() {
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
5554
|
+
try {
|
|
5555
|
+
const response = await this.apiService.getHelpCollections();
|
|
5556
|
+
if (response.status && response.data) {
|
|
5557
|
+
// Transform API response to local format
|
|
5558
|
+
return response.data.map((collection) => ({
|
|
5559
|
+
id: collection.id,
|
|
5560
|
+
title: collection.title || collection.name,
|
|
5561
|
+
description: collection.description || '',
|
|
5562
|
+
articleCount: collection.article_count || collection.articleCount || 0,
|
|
5563
|
+
icon: collection.icon || 'ph-book-open',
|
|
5564
|
+
url: collection.url || `#/help/${collection.slug || collection.id}`,
|
|
5565
|
+
}));
|
|
5566
|
+
}
|
|
5567
|
+
return [];
|
|
5568
|
+
} catch (error) {
|
|
5569
|
+
console.error('[MessengerWidget] Failed to fetch help articles:', error);
|
|
5570
|
+
return [];
|
|
5571
|
+
}
|
|
5572
|
+
}
|
|
5573
|
+
|
|
5574
|
+
/**
|
|
5575
|
+
* Fetch messages for a conversation
|
|
5576
|
+
*/
|
|
5577
|
+
async fetchMessages(conversationId) {
|
|
5578
|
+
try {
|
|
5579
|
+
const response = await this.apiService.getConversation(conversationId);
|
|
5580
|
+
if (response.status && response.data) {
|
|
5581
|
+
const messages = (response.data.messages || []).map((msg) => ({
|
|
5582
|
+
id: msg.id,
|
|
5583
|
+
content: msg.content,
|
|
5584
|
+
isOwn: msg.sender_type === 'customer',
|
|
5585
|
+
timestamp: msg.created_at,
|
|
5586
|
+
sender: {
|
|
5587
|
+
name: msg.sender_name || (msg.sender_type === 'customer' ? 'You' : 'Support'),
|
|
5588
|
+
avatarUrl: msg.sender_avatar || null,
|
|
5589
|
+
},
|
|
5590
|
+
}));
|
|
5591
|
+
this.messengerState.setMessages(conversationId, messages);
|
|
5592
|
+
|
|
5593
|
+
// Mark as read
|
|
5594
|
+
await this.apiService.markConversationAsRead(conversationId);
|
|
5595
|
+
this.messengerState.markAsRead(conversationId);
|
|
5596
|
+
|
|
5597
|
+
return messages;
|
|
5598
|
+
}
|
|
5599
|
+
return [];
|
|
5600
|
+
} catch (error) {
|
|
5601
|
+
console.error('[MessengerWidget] Failed to fetch messages:', error);
|
|
5602
|
+
return [];
|
|
5603
|
+
}
|
|
5604
|
+
}
|
|
5605
|
+
|
|
5606
|
+
/**
|
|
5607
|
+
* Start a new conversation
|
|
5608
|
+
*/
|
|
5609
|
+
async startNewConversation(message, subject = '') {
|
|
5610
|
+
try {
|
|
5611
|
+
const response = await this.apiService.startConversation({
|
|
5612
|
+
message,
|
|
5613
|
+
subject,
|
|
5614
|
+
});
|
|
5615
|
+
|
|
5616
|
+
if (response.status && response.data) {
|
|
5617
|
+
const conv = response.data;
|
|
5618
|
+
const newConversation = {
|
|
5619
|
+
id: conv.id,
|
|
5620
|
+
title: conv.subject || 'New conversation',
|
|
5621
|
+
participants: [{ name: 'Support', avatarUrl: null }],
|
|
5622
|
+
lastMessage: message,
|
|
5623
|
+
lastMessageTime: conv.created_at || new Date().toISOString(),
|
|
5624
|
+
unread: 0,
|
|
5625
|
+
status: 'open',
|
|
5626
|
+
};
|
|
5627
|
+
|
|
5628
|
+
// Add to state
|
|
5629
|
+
this.messengerState.addConversation(newConversation);
|
|
5630
|
+
|
|
5631
|
+
// Set initial message in messages cache
|
|
5632
|
+
this.messengerState.setMessages(conv.id, [
|
|
5633
|
+
{
|
|
5634
|
+
id: 'msg_' + Date.now(),
|
|
5635
|
+
content: message,
|
|
5636
|
+
isOwn: true,
|
|
5637
|
+
timestamp: new Date().toISOString(),
|
|
5638
|
+
},
|
|
5639
|
+
]);
|
|
5640
|
+
|
|
5641
|
+
// Navigate to chat
|
|
5642
|
+
this.messengerState.setActiveConversation(conv.id);
|
|
5643
|
+
this.messengerState.setView('chat');
|
|
5644
|
+
|
|
5645
|
+
return conv;
|
|
5646
|
+
}
|
|
5647
|
+
return null;
|
|
5648
|
+
} catch (error) {
|
|
5649
|
+
console.error('[MessengerWidget] Failed to start conversation:', error);
|
|
5650
|
+
return null;
|
|
4395
5651
|
}
|
|
5652
|
+
}
|
|
5653
|
+
|
|
5654
|
+
/**
|
|
5655
|
+
* Send typing indicator
|
|
5656
|
+
*/
|
|
5657
|
+
async sendTypingIndicator(conversationId, isTyping) {
|
|
5658
|
+
try {
|
|
5659
|
+
await this.apiService.sendTypingIndicator(conversationId, isTyping);
|
|
5660
|
+
} catch (error) {
|
|
5661
|
+
// Silently fail - typing indicators are not critical
|
|
5662
|
+
}
|
|
5663
|
+
}
|
|
4396
5664
|
|
|
4397
|
-
|
|
4398
|
-
|
|
5665
|
+
/**
|
|
5666
|
+
* Check agent availability
|
|
5667
|
+
*/
|
|
5668
|
+
async checkAgentAvailability() {
|
|
5669
|
+
try {
|
|
5670
|
+
const response = await this.apiService.checkAgentsOnline();
|
|
5671
|
+
if (response.status && response.data) {
|
|
5672
|
+
this.messengerState.agentsOnline = response.data.agents_online;
|
|
5673
|
+
this.messengerState.onlineCount = response.data.online_count || 0;
|
|
5674
|
+
this.messengerState.responseTime = response.data.response_time || '';
|
|
5675
|
+
this.messengerState._notify('availabilityUpdate', response.data);
|
|
5676
|
+
return response.data;
|
|
5677
|
+
}
|
|
5678
|
+
return { agents_online: false, online_count: 0 };
|
|
5679
|
+
} catch (error) {
|
|
5680
|
+
console.error('[MessengerWidget] Failed to check availability:', error);
|
|
5681
|
+
return { agents_online: false, online_count: 0 };
|
|
5682
|
+
}
|
|
4399
5683
|
}
|
|
4400
5684
|
|
|
4401
5685
|
async _fetchChangelog() {
|
|
@@ -4478,19 +5762,64 @@
|
|
|
4478
5762
|
};
|
|
4479
5763
|
}
|
|
4480
5764
|
|
|
4481
|
-
//
|
|
4482
|
-
|
|
5765
|
+
// Fetch changelogs from API
|
|
5766
|
+
const response = await this.apiService.getChangelogs({ limit: 20 });
|
|
5767
|
+
const changelogs = response.data || [];
|
|
5768
|
+
|
|
5769
|
+
// Map API response to expected format
|
|
5770
|
+
const mappedItems = changelogs.map((item) => ({
|
|
5771
|
+
id: item.id,
|
|
5772
|
+
title: item.title,
|
|
5773
|
+
description: item.excerpt || item.description || '',
|
|
5774
|
+
tags: item.labels ? item.labels.map((label) => label.name) : [],
|
|
5775
|
+
coverImage: item.cover_image || null,
|
|
5776
|
+
coverText: null,
|
|
5777
|
+
publishedAt: item.published_at,
|
|
5778
|
+
url: item.slug ? `/changelog/${item.slug}` : '#',
|
|
5779
|
+
}));
|
|
5780
|
+
|
|
5781
|
+
return {
|
|
5782
|
+
homeItems: mappedItems.slice(0, 3),
|
|
5783
|
+
changelogItems: mappedItems,
|
|
5784
|
+
};
|
|
4483
5785
|
}
|
|
4484
5786
|
|
|
4485
5787
|
onMount() {
|
|
4486
5788
|
// Load initial data after mounting
|
|
4487
5789
|
this.loadInitialData();
|
|
5790
|
+
|
|
5791
|
+
// Initialize WebSocket for real-time updates
|
|
5792
|
+
if (this.apiService?.sessionToken) {
|
|
5793
|
+
this._initWebSocket();
|
|
5794
|
+
}
|
|
5795
|
+
|
|
5796
|
+
// Check agent availability
|
|
5797
|
+
this.checkAgentAvailability();
|
|
5798
|
+
|
|
5799
|
+
// Periodically check availability (every 60 seconds)
|
|
5800
|
+
this._availabilityInterval = setInterval(() => {
|
|
5801
|
+
this.checkAgentAvailability();
|
|
5802
|
+
}, 60000);
|
|
4488
5803
|
}
|
|
4489
5804
|
|
|
4490
5805
|
onDestroy() {
|
|
4491
5806
|
if (this._stateUnsubscribe) {
|
|
4492
5807
|
this._stateUnsubscribe();
|
|
4493
5808
|
}
|
|
5809
|
+
|
|
5810
|
+
// Clean up WebSocket
|
|
5811
|
+
if (this.wsService) {
|
|
5812
|
+
this.wsService.disconnect();
|
|
5813
|
+
}
|
|
5814
|
+
|
|
5815
|
+
// Clean up WebSocket event listeners
|
|
5816
|
+
this._wsUnsubscribers.forEach((unsub) => unsub());
|
|
5817
|
+
this._wsUnsubscribers = [];
|
|
5818
|
+
|
|
5819
|
+
// Clean up availability interval
|
|
5820
|
+
if (this._availabilityInterval) {
|
|
5821
|
+
clearInterval(this._availabilityInterval);
|
|
5822
|
+
}
|
|
4494
5823
|
}
|
|
4495
5824
|
|
|
4496
5825
|
destroy() {
|
|
@@ -4500,6 +5829,7 @@
|
|
|
4500
5829
|
if (this.panel) {
|
|
4501
5830
|
this.panel.destroy();
|
|
4502
5831
|
}
|
|
5832
|
+
this.onDestroy();
|
|
4503
5833
|
super.destroy();
|
|
4504
5834
|
}
|
|
4505
5835
|
}
|
|
@@ -5586,6 +6916,54 @@
|
|
|
5586
6916
|
this.eventBus.emit('sdk:destroyed');
|
|
5587
6917
|
}
|
|
5588
6918
|
|
|
6919
|
+
/**
|
|
6920
|
+
* Check if feedback was recently submitted for this workspace
|
|
6921
|
+
* Uses backend tracking (preferred) with localStorage as fallback
|
|
6922
|
+
* @param {number} cooldownDays - Days to consider as "recently" (default: 30)
|
|
6923
|
+
* @returns {boolean} true if feedback was submitted within the cooldown period
|
|
6924
|
+
*/
|
|
6925
|
+
hasFeedbackBeenSubmitted(cooldownDays = 30) {
|
|
6926
|
+
const cooldownMs = cooldownDays * 24 * 60 * 60 * 1000;
|
|
6927
|
+
const now = Date.now();
|
|
6928
|
+
|
|
6929
|
+
// Check backend tracking first (from init response)
|
|
6930
|
+
if (this.config.last_feedback_at) {
|
|
6931
|
+
try {
|
|
6932
|
+
const backendTimestamp = new Date(this.config.last_feedback_at).getTime();
|
|
6933
|
+
if ((now - backendTimestamp) < cooldownMs) {
|
|
6934
|
+
return true;
|
|
6935
|
+
}
|
|
6936
|
+
} catch (e) {
|
|
6937
|
+
// Invalid date format, continue to localStorage check
|
|
6938
|
+
}
|
|
6939
|
+
}
|
|
6940
|
+
|
|
6941
|
+
// Fallback to localStorage
|
|
6942
|
+
try {
|
|
6943
|
+
const storageKey = `feedback_submitted_${this.config.workspace}`;
|
|
6944
|
+
const stored = localStorage.getItem(storageKey);
|
|
6945
|
+
if (!stored) return false;
|
|
6946
|
+
|
|
6947
|
+
const data = JSON.parse(stored);
|
|
6948
|
+
return (now - data.submittedAt) < cooldownMs;
|
|
6949
|
+
} catch (e) {
|
|
6950
|
+
return false;
|
|
6951
|
+
}
|
|
6952
|
+
}
|
|
6953
|
+
|
|
6954
|
+
/**
|
|
6955
|
+
* Clear the feedback submission tracking (allow showing widgets again)
|
|
6956
|
+
*/
|
|
6957
|
+
clearFeedbackSubmissionTracking() {
|
|
6958
|
+
try {
|
|
6959
|
+
const storageKey = `feedback_submitted_${this.config.workspace}`;
|
|
6960
|
+
localStorage.removeItem(storageKey);
|
|
6961
|
+
this.eventBus.emit('feedback:trackingCleared');
|
|
6962
|
+
} catch (e) {
|
|
6963
|
+
console.warn('Failed to clear feedback tracking:', e);
|
|
6964
|
+
}
|
|
6965
|
+
}
|
|
6966
|
+
|
|
5589
6967
|
_detectEnvironment() {
|
|
5590
6968
|
if (typeof window === 'undefined') {
|
|
5591
6969
|
return 'production';
|
|
@@ -7328,6 +8706,106 @@
|
|
|
7328
8706
|
}
|
|
7329
8707
|
}
|
|
7330
8708
|
|
|
8709
|
+
/* ========================================
|
|
8710
|
+
Availability Status
|
|
8711
|
+
======================================== */
|
|
8712
|
+
|
|
8713
|
+
.messenger-home-availability,
|
|
8714
|
+
.messenger-chat-availability {
|
|
8715
|
+
display: flex;
|
|
8716
|
+
align-items: center;
|
|
8717
|
+
gap: 6px;
|
|
8718
|
+
margin-top: 8px;
|
|
8719
|
+
font-size: 13px;
|
|
8720
|
+
color: rgba(255, 255, 255, 0.7);
|
|
8721
|
+
}
|
|
8722
|
+
|
|
8723
|
+
.theme-light .messenger-home-availability,
|
|
8724
|
+
.theme-light .messenger-chat-availability {
|
|
8725
|
+
color: #6b7280;
|
|
8726
|
+
}
|
|
8727
|
+
|
|
8728
|
+
.messenger-availability-dot {
|
|
8729
|
+
width: 8px;
|
|
8730
|
+
height: 8px;
|
|
8731
|
+
border-radius: 50%;
|
|
8732
|
+
flex-shrink: 0;
|
|
8733
|
+
}
|
|
8734
|
+
|
|
8735
|
+
.messenger-availability-online {
|
|
8736
|
+
background: #22c55e;
|
|
8737
|
+
box-shadow: 0 0 0 2px rgba(34, 197, 94, 0.2);
|
|
8738
|
+
}
|
|
8739
|
+
|
|
8740
|
+
.messenger-availability-away {
|
|
8741
|
+
background: #9ca3af;
|
|
8742
|
+
}
|
|
8743
|
+
|
|
8744
|
+
.messenger-availability-text {
|
|
8745
|
+
opacity: 0.9;
|
|
8746
|
+
}
|
|
8747
|
+
|
|
8748
|
+
/* ========================================
|
|
8749
|
+
Typing Indicator
|
|
8750
|
+
======================================== */
|
|
8751
|
+
|
|
8752
|
+
.messenger-typing-indicator {
|
|
8753
|
+
display: flex;
|
|
8754
|
+
align-items: center;
|
|
8755
|
+
gap: 8px;
|
|
8756
|
+
padding: 8px 12px;
|
|
8757
|
+
margin: 4px 0;
|
|
8758
|
+
}
|
|
8759
|
+
|
|
8760
|
+
.messenger-typing-dots {
|
|
8761
|
+
display: flex;
|
|
8762
|
+
align-items: center;
|
|
8763
|
+
gap: 4px;
|
|
8764
|
+
background: #374151;
|
|
8765
|
+
padding: 8px 12px;
|
|
8766
|
+
border-radius: 16px;
|
|
8767
|
+
}
|
|
8768
|
+
|
|
8769
|
+
.theme-light .messenger-typing-dots {
|
|
8770
|
+
background: #e5e7eb;
|
|
8771
|
+
}
|
|
8772
|
+
|
|
8773
|
+
.messenger-typing-dots span {
|
|
8774
|
+
width: 6px;
|
|
8775
|
+
height: 6px;
|
|
8776
|
+
background: #9ca3af;
|
|
8777
|
+
border-radius: 50%;
|
|
8778
|
+
animation: messenger-typing-bounce 1.4s infinite ease-in-out;
|
|
8779
|
+
}
|
|
8780
|
+
|
|
8781
|
+
.messenger-typing-dots span:nth-child(1) {
|
|
8782
|
+
animation-delay: -0.32s;
|
|
8783
|
+
}
|
|
8784
|
+
|
|
8785
|
+
.messenger-typing-dots span:nth-child(2) {
|
|
8786
|
+
animation-delay: -0.16s;
|
|
8787
|
+
}
|
|
8788
|
+
|
|
8789
|
+
.messenger-typing-dots span:nth-child(3) {
|
|
8790
|
+
animation-delay: 0s;
|
|
8791
|
+
}
|
|
8792
|
+
|
|
8793
|
+
.messenger-typing-text {
|
|
8794
|
+
font-size: 12px;
|
|
8795
|
+
color: #9ca3af;
|
|
8796
|
+
}
|
|
8797
|
+
|
|
8798
|
+
@keyframes messenger-typing-bounce {
|
|
8799
|
+
0%, 80%, 100% {
|
|
8800
|
+
transform: scale(0.8);
|
|
8801
|
+
opacity: 0.5;
|
|
8802
|
+
}
|
|
8803
|
+
40% {
|
|
8804
|
+
transform: scale(1);
|
|
8805
|
+
opacity: 1;
|
|
8806
|
+
}
|
|
8807
|
+
}
|
|
8808
|
+
|
|
7331
8809
|
/* ========================================
|
|
7332
8810
|
Animations
|
|
7333
8811
|
======================================== */
|
|
@@ -8286,8 +9764,8 @@
|
|
|
8286
9764
|
|
|
8287
9765
|
.changelog-popup-title {
|
|
8288
9766
|
margin: 0 0 16px;
|
|
8289
|
-
font-size:
|
|
8290
|
-
font-weight:
|
|
9767
|
+
font-size: 18px;
|
|
9768
|
+
font-weight: 600;
|
|
8291
9769
|
line-height: 1.3;
|
|
8292
9770
|
color: #111827;
|
|
8293
9771
|
}
|