@product7/feedback-sdk 1.2.6 → 1.2.7

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.
@@ -158,7 +158,9 @@
158
158
  id: 'conv_2',
159
159
  subject: 'Feature request',
160
160
  status: 'open',
161
- last_message_at: new Date(Date.now() - 6 * 24 * 60 * 60 * 1000).toISOString(),
161
+ last_message_at: new Date(
162
+ Date.now() - 6 * 24 * 60 * 60 * 1000
163
+ ).toISOString(),
162
164
  created_at: new Date(Date.now() - 10 * 24 * 60 * 60 * 1000).toISOString(),
163
165
  unread: 0,
164
166
  assigned_user: {
@@ -200,11 +202,14 @@
200
202
  id: 'msg_4',
201
203
  content: 'I would love to see a dark mode feature!',
202
204
  sender_type: 'customer',
203
- created_at: new Date(Date.now() - 6 * 24 * 60 * 60 * 1000 - 30 * 60 * 1000).toISOString(),
205
+ created_at: new Date(
206
+ Date.now() - 6 * 24 * 60 * 60 * 1000 - 30 * 60 * 1000
207
+ ).toISOString(),
204
208
  },
205
209
  {
206
210
  id: 'msg_5',
207
- content: "Great suggestion! That feature will be available next week. I'll let you know when it's ready.",
211
+ content:
212
+ "Great suggestion! That feature will be available next week. I'll let you know when it's ready.",
208
213
  sender_type: 'agent',
209
214
  sender_name: 'Tom',
210
215
  sender_avatar: null,
@@ -234,7 +239,8 @@
234
239
  {
235
240
  id: 'collection_3',
236
241
  title: 'AI Agent',
237
- description: 'Resolving customer questions instantly and accurately—from live chat to email.',
242
+ description:
243
+ 'Resolving customer questions instantly and accurately—from live chat to email.',
238
244
  articleCount: 82,
239
245
  icon: 'ph-robot',
240
246
  url: '#',
@@ -242,7 +248,8 @@
242
248
  {
243
249
  id: 'collection_4',
244
250
  title: 'Channels',
245
- description: 'Enabling the channels you use to communicate with customers, all from the Inbox.',
251
+ description:
252
+ 'Enabling the channels you use to communicate with customers, all from the Inbox.',
246
253
  articleCount: 45,
247
254
  icon: 'ph-chat-circle',
248
255
  url: '#',
@@ -789,10 +796,13 @@
789
796
  };
790
797
  }
791
798
 
792
- return this._makeRequest(`/widget/messenger/conversations/${conversationId}`, {
793
- method: 'GET',
794
- headers: { Authorization: `Bearer ${this.sessionToken}` },
795
- });
799
+ return this._makeRequest(
800
+ `/widget/messenger/conversations/${conversationId}`,
801
+ {
802
+ method: 'GET',
803
+ headers: { Authorization: `Bearer ${this.sessionToken}` },
804
+ }
805
+ );
796
806
  }
797
807
 
798
808
  /**
@@ -899,14 +909,17 @@
899
909
  return { status: true, data: newMessage };
900
910
  }
901
911
 
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
- });
912
+ return this._makeRequest(
913
+ `/widget/messenger/conversations/${conversationId}/messages`,
914
+ {
915
+ method: 'POST',
916
+ headers: {
917
+ 'Content-Type': 'application/json',
918
+ Authorization: `Bearer ${this.sessionToken}`,
919
+ },
920
+ body: JSON.stringify({ content: data.content }),
921
+ }
922
+ );
910
923
  }
911
924
 
912
925
  /**
@@ -924,14 +937,17 @@
924
937
  return { status: true };
925
938
  }
926
939
 
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
- });
940
+ return this._makeRequest(
941
+ `/widget/messenger/conversations/${conversationId}/typing`,
942
+ {
943
+ method: 'POST',
944
+ headers: {
945
+ 'Content-Type': 'application/json',
946
+ Authorization: `Bearer ${this.sessionToken}`,
947
+ },
948
+ body: JSON.stringify({ is_typing: isTyping }),
949
+ }
950
+ );
935
951
  }
936
952
 
937
953
  /**
@@ -948,10 +964,13 @@
948
964
  return { status: true };
949
965
  }
950
966
 
951
- return this._makeRequest(`/widget/messenger/conversations/${conversationId}/read`, {
952
- method: 'POST',
953
- headers: { Authorization: `Bearer ${this.sessionToken}` },
954
- });
967
+ return this._makeRequest(
968
+ `/widget/messenger/conversations/${conversationId}/read`,
969
+ {
970
+ method: 'POST',
971
+ headers: { Authorization: `Bearer ${this.sessionToken}` },
972
+ }
973
+ );
955
974
  }
956
975
 
957
976
  /**
@@ -964,7 +983,10 @@
964
983
  }
965
984
 
966
985
  if (this.mock) {
967
- const count = MOCK_CONVERSATIONS.reduce((sum, c) => sum + (c.unread || 0), 0);
986
+ const count = MOCK_CONVERSATIONS.reduce(
987
+ (sum, c) => sum + (c.unread || 0),
988
+ 0
989
+ );
968
990
  return {
969
991
  status: true,
970
992
  data: { unread_count: count, unread_conversations: count > 0 ? 1 : 0 },
@@ -994,17 +1016,20 @@
994
1016
  return { status: true, message: 'Thank you for your feedback!' };
995
1017
  }
996
1018
 
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
- });
1019
+ return this._makeRequest(
1020
+ `/widget/messenger/conversations/${conversationId}/rate`,
1021
+ {
1022
+ method: 'POST',
1023
+ headers: {
1024
+ 'Content-Type': 'application/json',
1025
+ Authorization: `Bearer ${this.sessionToken}`,
1026
+ },
1027
+ body: JSON.stringify({
1028
+ rating: data.rating,
1029
+ comment: data.comment || '',
1030
+ }),
1031
+ }
1032
+ );
1008
1033
  }
1009
1034
 
1010
1035
  /**
@@ -1602,7 +1627,7 @@
1602
1627
  if (this.options.suppressAfterSubmission && this._hasRecentlySubmitted()) {
1603
1628
  this.sdk.eventBus.emit('widget:suppressed', {
1604
1629
  widget: this,
1605
- reason: 'recently_submitted'
1630
+ reason: 'recently_submitted',
1606
1631
  });
1607
1632
  return this;
1608
1633
  }
@@ -1826,8 +1851,10 @@
1826
1851
  // Check backend tracking first (from init response)
1827
1852
  if (this.sdk.config.last_feedback_at) {
1828
1853
  try {
1829
- const backendTimestamp = new Date(this.sdk.config.last_feedback_at).getTime();
1830
- if ((now - backendTimestamp) < cooldownMs) {
1854
+ const backendTimestamp = new Date(
1855
+ this.sdk.config.last_feedback_at
1856
+ ).getTime();
1857
+ if (now - backendTimestamp < cooldownMs) {
1831
1858
  return true;
1832
1859
  }
1833
1860
  } catch (e) {
@@ -1846,7 +1873,7 @@
1846
1873
  const data = JSON.parse(stored);
1847
1874
  const submittedAt = data.submittedAt;
1848
1875
 
1849
- return (now - submittedAt) < cooldownMs;
1876
+ return now - submittedAt < cooldownMs;
1850
1877
  } catch (e) {
1851
1878
  // localStorage may not be available or data is corrupted
1852
1879
  return false;
@@ -2971,135 +2998,411 @@
2971
2998
  }
2972
2999
 
2973
3000
  /**
2974
- * MessengerState - State management for the Messenger widget
3001
+ * WebSocketService - Real-time communication for messenger widget
2975
3002
  */
2976
- class MessengerState {
2977
- constructor(options = {}) {
2978
- this.currentView = 'home'; // 'home', 'messages', 'chat', 'help', 'changelog'
2979
- this.isOpen = false;
2980
- this.unreadCount = 0;
2981
- this.activeConversationId = null;
2982
-
2983
- // Conversations
2984
- this.conversations = [];
2985
- this.messages = {}; // { conversationId: [messages] }
2986
3003
 
2987
- // Help articles
2988
- this.helpArticles = [];
2989
- this.helpSearchQuery = '';
3004
+ class WebSocketService {
3005
+ constructor(config = {}) {
3006
+ this.baseURL = config.baseURL || '';
3007
+ this.workspace = config.workspace || '';
3008
+ this.sessionToken = config.sessionToken || null;
3009
+ this.mock = config.mock || false;
2990
3010
 
2991
- // Changelog
2992
- this.homeChangelogItems = [];
2993
- this.changelogItems = [];
3011
+ this.ws = null;
3012
+ this.reconnectAttempts = 0;
3013
+ this.maxReconnectAttempts = 5;
3014
+ this.reconnectDelay = 1000;
3015
+ this.pingInterval = null;
3016
+ this.isConnected = false;
2994
3017
 
2995
- // Team info
2996
- this.teamName = options.teamName || 'Support';
2997
- this.teamAvatars = options.teamAvatars || [];
2998
- this.welcomeMessage = options.welcomeMessage || 'How can we help?';
3018
+ // Event listeners
3019
+ this._listeners = new Map();
2999
3020
 
3000
- // User info
3001
- this.userContext = options.userContext || null;
3021
+ // Bind methods
3022
+ this._onOpen = this._onOpen.bind(this);
3023
+ this._onMessage = this._onMessage.bind(this);
3024
+ this._onClose = this._onClose.bind(this);
3025
+ this._onError = this._onError.bind(this);
3026
+ }
3002
3027
 
3003
- // Feature flags
3004
- this.enableHelp = options.enableHelp !== false;
3005
- this.enableChangelog = options.enableChangelog !== false;
3028
+ /**
3029
+ * Connect to WebSocket server
3030
+ */
3031
+ connect(sessionToken = null) {
3032
+ if (sessionToken) {
3033
+ this.sessionToken = sessionToken;
3034
+ }
3006
3035
 
3007
- // Agent availability
3008
- this.agentsOnline = false;
3009
- this.onlineCount = 0;
3010
- this.responseTime = 'Usually replies within a few minutes';
3036
+ if (!this.sessionToken) {
3037
+ console.warn('[WebSocket] No session token provided');
3038
+ return;
3039
+ }
3011
3040
 
3012
- // Typing indicators
3013
- this.typingUsers = {}; // { conversationId: { userName, timestamp } }
3041
+ // Mock mode - simulate connection
3042
+ if (this.mock) {
3043
+ this.isConnected = true;
3044
+ this._emit('connected', {});
3045
+ this._startMockResponses();
3046
+ return;
3047
+ }
3014
3048
 
3015
- // Loading states
3016
- this.isLoading = false;
3017
- this.isLoadingMessages = false;
3049
+ // Build WebSocket URL
3050
+ const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
3051
+ let wsURL = this.baseURL.replace(/^https?:/, wsProtocol);
3052
+ wsURL = wsURL.replace('/api/v1', '');
3053
+ wsURL = `${wsURL}/api/v1/widget/messenger/ws?token=${encodeURIComponent(this.sessionToken)}`;
3018
3054
 
3019
- // Listeners
3020
- this._listeners = new Set();
3055
+ try {
3056
+ this.ws = new WebSocket(wsURL);
3057
+ this.ws.onopen = this._onOpen;
3058
+ this.ws.onmessage = this._onMessage;
3059
+ this.ws.onclose = this._onClose;
3060
+ this.ws.onerror = this._onError;
3061
+ } catch (error) {
3062
+ console.error('[WebSocket] Connection error:', error);
3063
+ this._scheduleReconnect();
3064
+ }
3021
3065
  }
3022
3066
 
3023
3067
  /**
3024
- * Subscribe to state changes
3068
+ * Disconnect from WebSocket server
3025
3069
  */
3026
- subscribe(callback) {
3027
- this._listeners.add(callback);
3028
- return () => this._listeners.delete(callback);
3029
- }
3070
+ disconnect() {
3071
+ this.isConnected = false;
3072
+ this.reconnectAttempts = this.maxReconnectAttempts; // Prevent reconnection
3030
3073
 
3031
- /**
3032
- * Notify all listeners of state change
3033
- */
3034
- _notify(changeType, data) {
3035
- this._listeners.forEach((cb) => cb(changeType, data, this));
3036
- }
3074
+ if (this.pingInterval) {
3075
+ clearInterval(this.pingInterval);
3076
+ this.pingInterval = null;
3077
+ }
3037
3078
 
3038
- /**
3039
- * Set current view
3040
- */
3041
- setView(view) {
3042
- const previousView = this.currentView;
3043
- this.currentView = view;
3044
- this._notify('viewChange', { previousView, currentView: view });
3045
- }
3079
+ if (this.ws) {
3080
+ this.ws.close();
3081
+ this.ws = null;
3082
+ }
3046
3083
 
3047
- /**
3048
- * Toggle panel open/closed
3049
- */
3050
- setOpen(isOpen) {
3051
- this.isOpen = isOpen;
3052
- this._notify('openChange', { isOpen });
3084
+ if (this._mockInterval) {
3085
+ clearInterval(this._mockInterval);
3086
+ this._mockInterval = null;
3087
+ }
3053
3088
  }
3054
3089
 
3055
3090
  /**
3056
- * Set active conversation for chat view
3091
+ * Subscribe to events
3092
+ * @param {string} event - Event name
3093
+ * @param {Function} callback - Event handler
3094
+ * @returns {Function} Unsubscribe function
3057
3095
  */
3058
- setActiveConversation(conversationId) {
3059
- this.activeConversationId = conversationId;
3060
- this._notify('conversationChange', { conversationId });
3096
+ on(event, callback) {
3097
+ if (!this._listeners.has(event)) {
3098
+ this._listeners.set(event, new Set());
3099
+ }
3100
+ this._listeners.get(event).add(callback);
3101
+ return () => this._listeners.get(event).delete(callback);
3061
3102
  }
3062
3103
 
3063
3104
  /**
3064
- * Update conversations list
3105
+ * Remove event listener
3065
3106
  */
3066
- setConversations(conversations) {
3067
- this.conversations = conversations;
3068
- this._updateUnreadCount();
3069
- this._notify('conversationsUpdate', { conversations });
3107
+ off(event, callback) {
3108
+ if (this._listeners.has(event)) {
3109
+ this._listeners.get(event).delete(callback);
3110
+ }
3070
3111
  }
3071
3112
 
3072
3113
  /**
3073
- * Add a new conversation
3114
+ * Send message through WebSocket
3074
3115
  */
3075
- addConversation(conversation) {
3076
- this.conversations.unshift(conversation);
3077
- this._updateUnreadCount();
3078
- this._notify('conversationAdded', { conversation });
3079
- }
3116
+ send(type, payload = {}) {
3117
+ if (!this.isConnected) {
3118
+ console.warn('[WebSocket] Not connected, cannot send message');
3119
+ return;
3120
+ }
3080
3121
 
3081
- /**
3082
- * Update messages for a conversation
3083
- */
3084
- setMessages(conversationId, messages) {
3085
- this.messages[conversationId] = messages;
3086
- this._notify('messagesUpdate', { conversationId, messages });
3087
- }
3122
+ if (this.mock) {
3123
+ // Mock mode - just log
3124
+ console.log('[WebSocket Mock] Sending:', type, payload);
3125
+ return;
3126
+ }
3088
3127
 
3089
- /**
3090
- * Add a message to a conversation
3091
- */
3092
- addMessage(conversationId, message) {
3093
- if (!this.messages[conversationId]) {
3094
- this.messages[conversationId] = [];
3128
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
3129
+ this.ws.send(JSON.stringify({ type, payload }));
3095
3130
  }
3096
- this.messages[conversationId].push(message);
3131
+ }
3097
3132
 
3098
- // Update conversation preview
3099
- const conv = this.conversations.find((c) => c.id === conversationId);
3100
- if (conv) {
3101
- conv.lastMessage = message.content;
3102
- conv.lastMessageTime = message.timestamp;
3133
+ // Private methods
3134
+
3135
+ _onOpen() {
3136
+ console.log('[WebSocket] Connected');
3137
+ this.isConnected = true;
3138
+ this.reconnectAttempts = 0;
3139
+ this._emit('connected', {});
3140
+
3141
+ // Start ping interval to keep connection alive
3142
+ this.pingInterval = setInterval(() => {
3143
+ this.send('ping', {});
3144
+ }, 30000);
3145
+ }
3146
+
3147
+ _onMessage(event) {
3148
+ try {
3149
+ const data = JSON.parse(event.data);
3150
+ const { type, payload } = data;
3151
+
3152
+ // Handle different event types
3153
+ switch (type) {
3154
+ case 'message:new':
3155
+ this._emit('message', payload);
3156
+ break;
3157
+ case 'typing:started':
3158
+ this._emit('typing_started', payload);
3159
+ break;
3160
+ case 'typing:stopped':
3161
+ this._emit('typing_stopped', payload);
3162
+ break;
3163
+ case 'conversation:updated':
3164
+ this._emit('conversation_updated', payload);
3165
+ break;
3166
+ case 'conversation:closed':
3167
+ this._emit('conversation_closed', payload);
3168
+ break;
3169
+ case 'availability:changed':
3170
+ this._emit('availability_changed', payload);
3171
+ break;
3172
+ case 'pong':
3173
+ // Ping response, ignore
3174
+ break;
3175
+ default:
3176
+ console.log('[WebSocket] Unknown event:', type, payload);
3177
+ }
3178
+ } catch (error) {
3179
+ console.error('[WebSocket] Failed to parse message:', error);
3180
+ }
3181
+ }
3182
+
3183
+ _onClose(event) {
3184
+ console.log('[WebSocket] Disconnected:', event.code, event.reason);
3185
+ this.isConnected = false;
3186
+
3187
+ if (this.pingInterval) {
3188
+ clearInterval(this.pingInterval);
3189
+ this.pingInterval = null;
3190
+ }
3191
+
3192
+ this._emit('disconnected', { code: event.code, reason: event.reason });
3193
+ this._scheduleReconnect();
3194
+ }
3195
+
3196
+ _onError(error) {
3197
+ console.error('[WebSocket] Error:', error);
3198
+ this._emit('error', { error });
3199
+ }
3200
+
3201
+ _scheduleReconnect() {
3202
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) {
3203
+ console.log('[WebSocket] Max reconnect attempts reached');
3204
+ this._emit('reconnect_failed', {});
3205
+ return;
3206
+ }
3207
+
3208
+ this.reconnectAttempts++;
3209
+ const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
3210
+ console.log(
3211
+ `[WebSocket] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`
3212
+ );
3213
+
3214
+ setTimeout(() => {
3215
+ this.connect();
3216
+ }, delay);
3217
+ }
3218
+
3219
+ _emit(event, data) {
3220
+ if (this._listeners.has(event)) {
3221
+ this._listeners.get(event).forEach((callback) => {
3222
+ try {
3223
+ callback(data);
3224
+ } catch (error) {
3225
+ console.error(`[WebSocket] Error in ${event} handler:`, error);
3226
+ }
3227
+ });
3228
+ }
3229
+ }
3230
+
3231
+ // Mock support for development
3232
+ _startMockResponses() {
3233
+ // Simulate agent typing and responses
3234
+ this._mockInterval = setInterval(() => {
3235
+ // Randomly emit typing or message events for demo
3236
+ const random = Math.random();
3237
+ if (random < 0.1) {
3238
+ this._emit('typing_started', {
3239
+ conversation_id: 'conv_1',
3240
+ user_id: 'agent_1',
3241
+ user_name: 'Sarah',
3242
+ is_agent: true,
3243
+ });
3244
+
3245
+ // Stop typing after 2 seconds
3246
+ setTimeout(() => {
3247
+ this._emit('typing_stopped', {
3248
+ conversation_id: 'conv_1',
3249
+ user_id: 'agent_1',
3250
+ });
3251
+ }, 2000);
3252
+ }
3253
+ }, 10000);
3254
+ }
3255
+
3256
+ /**
3257
+ * Simulate receiving a message (for mock mode)
3258
+ */
3259
+ simulateMessage(conversationId, message) {
3260
+ if (this.mock) {
3261
+ this._emit('message', {
3262
+ conversation_id: conversationId,
3263
+ message: {
3264
+ id: 'msg_' + Date.now(),
3265
+ content: message.content,
3266
+ sender_type: message.sender_type || 'agent',
3267
+ sender_name: message.sender_name || 'Support',
3268
+ sender_avatar: message.sender_avatar || null,
3269
+ created_at: new Date().toISOString(),
3270
+ },
3271
+ });
3272
+ }
3273
+ }
3274
+ }
3275
+
3276
+ /**
3277
+ * MessengerState - State management for the Messenger widget
3278
+ */
3279
+ class MessengerState {
3280
+ constructor(options = {}) {
3281
+ this.currentView = 'home'; // 'home', 'messages', 'chat', 'help', 'changelog'
3282
+ this.isOpen = false;
3283
+ this.unreadCount = 0;
3284
+ this.activeConversationId = null;
3285
+
3286
+ // Conversations
3287
+ this.conversations = [];
3288
+ this.messages = {}; // { conversationId: [messages] }
3289
+
3290
+ // Help articles
3291
+ this.helpArticles = [];
3292
+ this.helpSearchQuery = '';
3293
+
3294
+ // Changelog
3295
+ this.homeChangelogItems = [];
3296
+ this.changelogItems = [];
3297
+
3298
+ // Team info
3299
+ this.teamName = options.teamName || 'Support';
3300
+ this.teamAvatars = options.teamAvatars || [];
3301
+ this.welcomeMessage = options.welcomeMessage || 'How can we help?';
3302
+
3303
+ // User info
3304
+ this.userContext = options.userContext || null;
3305
+
3306
+ // Feature flags
3307
+ this.enableHelp = options.enableHelp !== false;
3308
+ this.enableChangelog = options.enableChangelog !== false;
3309
+
3310
+ // Agent availability
3311
+ this.agentsOnline = false;
3312
+ this.onlineCount = 0;
3313
+ this.responseTime = 'Usually replies within a few minutes';
3314
+
3315
+ // Typing indicators
3316
+ this.typingUsers = {}; // { conversationId: { userName, timestamp } }
3317
+
3318
+ // Loading states
3319
+ this.isLoading = false;
3320
+ this.isLoadingMessages = false;
3321
+
3322
+ // Listeners
3323
+ this._listeners = new Set();
3324
+ }
3325
+
3326
+ /**
3327
+ * Subscribe to state changes
3328
+ */
3329
+ subscribe(callback) {
3330
+ this._listeners.add(callback);
3331
+ return () => this._listeners.delete(callback);
3332
+ }
3333
+
3334
+ /**
3335
+ * Notify all listeners of state change
3336
+ */
3337
+ _notify(changeType, data) {
3338
+ this._listeners.forEach((cb) => cb(changeType, data, this));
3339
+ }
3340
+
3341
+ /**
3342
+ * Set current view
3343
+ */
3344
+ setView(view) {
3345
+ const previousView = this.currentView;
3346
+ this.currentView = view;
3347
+ this._notify('viewChange', { previousView, currentView: view });
3348
+ }
3349
+
3350
+ /**
3351
+ * Toggle panel open/closed
3352
+ */
3353
+ setOpen(isOpen) {
3354
+ this.isOpen = isOpen;
3355
+ this._notify('openChange', { isOpen });
3356
+ }
3357
+
3358
+ /**
3359
+ * Set active conversation for chat view
3360
+ */
3361
+ setActiveConversation(conversationId) {
3362
+ this.activeConversationId = conversationId;
3363
+ this._notify('conversationChange', { conversationId });
3364
+ }
3365
+
3366
+ /**
3367
+ * Update conversations list
3368
+ */
3369
+ setConversations(conversations) {
3370
+ this.conversations = conversations;
3371
+ this._updateUnreadCount();
3372
+ this._notify('conversationsUpdate', { conversations });
3373
+ }
3374
+
3375
+ /**
3376
+ * Add a new conversation
3377
+ */
3378
+ addConversation(conversation) {
3379
+ this.conversations.unshift(conversation);
3380
+ this._updateUnreadCount();
3381
+ this._notify('conversationAdded', { conversation });
3382
+ }
3383
+
3384
+ /**
3385
+ * Update messages for a conversation
3386
+ */
3387
+ setMessages(conversationId, messages) {
3388
+ this.messages[conversationId] = messages;
3389
+ this._notify('messagesUpdate', { conversationId, messages });
3390
+ }
3391
+
3392
+ /**
3393
+ * Add a message to a conversation
3394
+ */
3395
+ addMessage(conversationId, message) {
3396
+ if (!this.messages[conversationId]) {
3397
+ this.messages[conversationId] = [];
3398
+ }
3399
+ this.messages[conversationId].push(message);
3400
+
3401
+ // Update conversation preview
3402
+ const conv = this.conversations.find((c) => c.id === conversationId);
3403
+ if (conv) {
3404
+ conv.lastMessage = message.content;
3405
+ conv.lastMessageTime = message.timestamp;
3103
3406
  if (!message.isOwn) {
3104
3407
  conv.unread = (conv.unread || 0) + 1;
3105
3408
  this._updateUnreadCount();
@@ -3828,7 +4131,10 @@
3828
4131
  data.conversationId === this.state.activeConversationId
3829
4132
  ) {
3830
4133
  this._hideTypingIndicator();
3831
- } else if (type === 'messagesUpdate' && data.conversationId === this.state.activeConversationId) {
4134
+ } else if (
4135
+ type === 'messagesUpdate' &&
4136
+ data.conversationId === this.state.activeConversationId
4137
+ ) {
3832
4138
  this._updateContent();
3833
4139
  }
3834
4140
  });
@@ -3888,14 +4194,17 @@
3888
4194
  </div>
3889
4195
  `;
3890
4196
 
3891
- this._typingIndicator = this.element.querySelector('.messenger-typing-indicator');
4197
+ this._typingIndicator = this.element.querySelector(
4198
+ '.messenger-typing-indicator'
4199
+ );
3892
4200
  this._attachEvents();
3893
4201
  this._scrollToBottom();
3894
4202
  }
3895
4203
 
3896
4204
  _renderEmptyState(isNewConversation = false) {
3897
4205
  const avatarHtml = this._renderTeamAvatars();
3898
- const responseTime = this.state.responseTime || 'We typically reply within a few minutes';
4206
+ const responseTime =
4207
+ this.state.responseTime || 'We typically reply within a few minutes';
3899
4208
  const isOnline = this.state.agentsOnline;
3900
4209
 
3901
4210
  return `
@@ -4151,7 +4460,9 @@
4151
4460
  _showTypingIndicator(userName) {
4152
4461
  if (this._typingIndicator) {
4153
4462
  this._typingIndicator.style.display = 'flex';
4154
- const textEl = this._typingIndicator.querySelector('.messenger-typing-text');
4463
+ const textEl = this._typingIndicator.querySelector(
4464
+ '.messenger-typing-text'
4465
+ );
4155
4466
  if (textEl) {
4156
4467
  textEl.textContent = `${userName || 'Support'} is typing...`;
4157
4468
  }
@@ -4532,255 +4843,19 @@
4532
4843
  <p>Try a different search term</p>
4533
4844
  </div>
4534
4845
  `;
4535
- }
4536
-
4537
- return `
4538
- <div class="messenger-help-empty">
4539
- <div class="messenger-help-empty-icon">
4540
- <i class="ph ph-question" style="font-size: 48px;"></i>
4541
- </div>
4542
- <h3>Help collections</h3>
4543
- <p>No collections available yet</p>
4544
- </div>
4545
- `;
4546
- }
4547
-
4548
- _attachEvents() {
4549
- // Close button
4550
- this.element
4551
- .querySelector('.messenger-close-btn')
4552
- .addEventListener('click', () => {
4553
- this.state.setOpen(false);
4554
- });
4555
-
4556
- // Search input
4557
- const searchInput = this.element.querySelector(
4558
- '.messenger-help-search-input'
4559
- );
4560
- let searchTimeout;
4561
- searchInput.addEventListener('input', (e) => {
4562
- clearTimeout(searchTimeout);
4563
- searchTimeout = setTimeout(() => {
4564
- this.state.setHelpSearchQuery(e.target.value);
4565
- }, 300);
4566
- });
4567
-
4568
- this._attachCollectionEvents();
4569
- }
4570
-
4571
- _attachCollectionEvents() {
4572
- this.element
4573
- .querySelectorAll('.messenger-help-collection')
4574
- .forEach((item) => {
4575
- item.addEventListener('click', () => {
4576
- const collectionId = item.dataset.collectionId;
4577
- const collection = this.state.helpArticles.find(
4578
- (c) => c.id === collectionId
4579
- );
4580
- if (collection && collection.url) {
4581
- window.open(collection.url, '_blank');
4582
- } else if (this.options.onArticleClick) {
4583
- this.options.onArticleClick(collection);
4584
- }
4585
- });
4586
- });
4587
- }
4588
-
4589
- destroy() {
4590
- if (this._unsubscribe) {
4591
- this._unsubscribe();
4592
- }
4593
- if (this.element && this.element.parentNode) {
4594
- this.element.parentNode.removeChild(this.element);
4595
- }
4596
- }
4597
- }
4598
-
4599
- /**
4600
- * HomeView - Welcome screen with team info and quick actions
4601
- */
4602
- class HomeView {
4603
- constructor(state, options = {}) {
4604
- this.state = state;
4605
- this.options = options;
4606
- this.element = null;
4607
- this._unsubscribe = null;
4608
- }
4609
-
4610
- render() {
4611
- this.element = document.createElement('div');
4612
- this.element.className = 'messenger-view messenger-home-view';
4613
-
4614
- this._updateContent();
4615
-
4616
- // Subscribe to state changes to re-render when data loads
4617
- this._unsubscribe = this.state.subscribe((type) => {
4618
- if (type === 'homeChangelogUpdate' || type === 'conversationsUpdate' || type === 'availabilityUpdate') {
4619
- this._updateContent();
4620
- }
4621
- });
4622
-
4623
- return this.element;
4624
- }
4625
-
4626
- _updateContent() {
4627
- const avatarsHtml = this._renderAvatarStack();
4628
- const recentChangelogHtml = this._renderRecentChangelog();
4629
-
4630
- this.element.innerHTML = `
4631
- <div class="messenger-home-header">
4632
- <div class="messenger-home-header-top">
4633
- <div class="messenger-home-logo">
4634
- ${this.options.logoUrl ? `<img src="${this.options.logoUrl}" alt="${this.state.teamName}" />` : ''}
4635
- </div>
4636
- <div class="messenger-home-avatars">${avatarsHtml}</div>
4637
- <button class="messenger-close-btn" aria-label="Close">
4638
- <i class="ph ph-x" style="font-size: 20px;"></i>
4639
- </button>
4640
- </div>
4641
- <div class="messenger-home-welcome">
4642
- <span class="messenger-home-greeting">Hello there.</span>
4643
- <span class="messenger-home-question">${this.state.welcomeMessage}</span>
4644
- ${this._renderAvailabilityStatus()}
4645
- </div>
4646
- </div>
4647
-
4648
- <div class="messenger-home-body">
4649
- <button class="messenger-home-message-btn">
4650
- <span>Send us a message</span>
4651
- <i class="ph ph-arrow-right" style="font-size: 16px;"></i>
4652
- </button>
4653
-
4654
- ${this._renderFeaturedCard()}
4655
-
4656
- ${recentChangelogHtml}
4657
- </div>
4658
- `;
4659
-
4660
- this._attachEvents();
4661
- }
4662
-
4663
- _renderAvatarStack() {
4664
- const avatars = this.state.teamAvatars;
4665
- if (!avatars || avatars.length === 0) {
4666
- // Default avatars with initials
4667
- return `
4668
- <div class="messenger-avatar-stack">
4669
- <div class="messenger-avatar" style="background: #5856d6;">S</div>
4670
- <div class="messenger-avatar" style="background: #007aff;">T</div>
4671
- </div>
4672
- `;
4673
- }
4674
-
4675
- const avatarItems = avatars
4676
- .slice(0, 3)
4677
- .map((avatar, i) => {
4678
- if (typeof avatar === 'string' && avatar.startsWith('http')) {
4679
- return `<img class="messenger-avatar" src="${avatar}" alt="Team member" style="z-index: ${3 - i};" />`;
4680
- }
4681
- return `<div class="messenger-avatar" style="background: ${this._getAvatarColor(i)}; z-index: ${3 - i};">${avatar.charAt(0).toUpperCase()}</div>`;
4682
- })
4683
- .join('');
4684
-
4685
- return `<div class="messenger-avatar-stack">${avatarItems}</div>`;
4686
- }
4687
-
4688
- _getAvatarColor(index) {
4689
- const colors = ['#5856d6', '#007aff', '#34c759', '#ff9500', '#ff3b30'];
4690
- return colors[index % colors.length];
4691
- }
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
-
4714
- _renderFeaturedCard() {
4715
- // Only show if there's featured content configured
4716
- if (!this.options.featuredContent) {
4717
- return '';
4718
- }
4719
-
4720
- const { title, description, imageUrl, action } =
4721
- this.options.featuredContent;
4722
-
4723
- return `
4724
- <div class="messenger-home-featured">
4725
- ${imageUrl ? `<img src="${imageUrl}" alt="${title}" class="messenger-home-featured-image" onerror="this.style.display='none';" />` : ''}
4726
- <div class="messenger-home-featured-content">
4727
- <h3>${title}</h3>
4728
- <p>${description}</p>
4729
- </div>
4730
- ${action ? `<button class="messenger-home-featured-btn" data-action="${action.type}" data-value="${action.value}">${action.label}</button>` : ''}
4731
- </div>
4732
- `;
4733
- }
4734
-
4735
- _renderRecentChangelog() {
4736
- // Show recent changelog preview as cards with images
4737
- const changelogItems = this.state.homeChangelogItems;
4738
- if (changelogItems.length === 0) {
4739
- return '';
4740
- }
4741
-
4742
- const changelogHtml = changelogItems
4743
- .map(
4744
- (item) => `
4745
- <div class="messenger-home-changelog-card" data-changelog-id="${item.id}">
4746
- ${
4747
- item.coverImage
4748
- ? `
4749
- <div class="messenger-home-changelog-cover">
4750
- <img src="${item.coverImage}" alt="${item.title}" onerror="this.style.display='none';" />
4751
- ${item.coverText ? `<span class="messenger-home-changelog-cover-text">${item.coverText}</span>` : ''}
4752
- </div>
4753
- `
4754
- : ''
4755
- }
4756
- <div class="messenger-home-changelog-card-content">
4757
- <h4 class="messenger-home-changelog-card-title">${item.title}</h4>
4758
- <p class="messenger-home-changelog-card-desc">${item.description || ''}</p>
4759
- </div>
4760
- </div>
4761
- `
4762
- )
4763
- .join('');
4846
+ }
4764
4847
 
4765
4848
  return `
4766
- <div class="messenger-home-changelog-section">
4767
- ${changelogHtml}
4849
+ <div class="messenger-help-empty">
4850
+ <div class="messenger-help-empty-icon">
4851
+ <i class="ph ph-question" style="font-size: 48px;"></i>
4852
+ </div>
4853
+ <h3>Help collections</h3>
4854
+ <p>No collections available yet</p>
4768
4855
  </div>
4769
4856
  `;
4770
4857
  }
4771
4858
 
4772
- _formatDate(dateString) {
4773
- if (!dateString) return '';
4774
- const date = new Date(dateString);
4775
- const now = new Date();
4776
- const diffDays = Math.floor((now - date) / (1000 * 60 * 60 * 24));
4777
-
4778
- if (diffDays === 0) return 'Today';
4779
- if (diffDays === 1) return 'Yesterday';
4780
- if (diffDays < 7) return `${diffDays}d ago`;
4781
- return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
4782
- }
4783
-
4784
4859
  _attachEvents() {
4785
4860
  // Close button
4786
4861
  this.element
@@ -4789,48 +4864,37 @@
4789
4864
  this.state.setOpen(false);
4790
4865
  });
4791
4866
 
4792
- // Send message button
4793
- this.element
4794
- .querySelector('.messenger-home-message-btn')
4795
- .addEventListener('click', () => {
4796
- this.state.setView('messages');
4797
- });
4867
+ // Search input
4868
+ const searchInput = this.element.querySelector(
4869
+ '.messenger-help-search-input'
4870
+ );
4871
+ let searchTimeout;
4872
+ searchInput.addEventListener('input', (e) => {
4873
+ clearTimeout(searchTimeout);
4874
+ searchTimeout = setTimeout(() => {
4875
+ this.state.setHelpSearchQuery(e.target.value);
4876
+ }, 300);
4877
+ });
4798
4878
 
4799
- // Changelog items
4879
+ this._attachCollectionEvents();
4880
+ }
4881
+
4882
+ _attachCollectionEvents() {
4800
4883
  this.element
4801
- .querySelectorAll('.messenger-home-changelog-item')
4884
+ .querySelectorAll('.messenger-help-collection')
4802
4885
  .forEach((item) => {
4803
4886
  item.addEventListener('click', () => {
4804
- // Navigate to changelog view with specific item selected
4805
- this.state.setView('changelog');
4887
+ const collectionId = item.dataset.collectionId;
4888
+ const collection = this.state.helpArticles.find(
4889
+ (c) => c.id === collectionId
4890
+ );
4891
+ if (collection && collection.url) {
4892
+ window.open(collection.url, '_blank');
4893
+ } else if (this.options.onArticleClick) {
4894
+ this.options.onArticleClick(collection);
4895
+ }
4806
4896
  });
4807
4897
  });
4808
-
4809
- // See all changelog
4810
- const seeAllBtn = this.element.querySelector(
4811
- '.messenger-home-changelog-all'
4812
- );
4813
- if (seeAllBtn) {
4814
- seeAllBtn.addEventListener('click', () => {
4815
- this.state.setView('changelog');
4816
- });
4817
- }
4818
-
4819
- // Featured card action
4820
- const featuredBtn = this.element.querySelector(
4821
- '.messenger-home-featured-btn'
4822
- );
4823
- if (featuredBtn) {
4824
- featuredBtn.addEventListener('click', () => {
4825
- const action = featuredBtn.dataset.action;
4826
- const value = featuredBtn.dataset.value;
4827
- if (action === 'url') {
4828
- window.open(value, '_blank');
4829
- } else if (action === 'view') {
4830
- this.state.setView(value);
4831
- }
4832
- });
4833
- }
4834
4898
  }
4835
4899
 
4836
4900
  destroy() {
@@ -4844,277 +4908,255 @@
4844
4908
  }
4845
4909
 
4846
4910
  /**
4847
- * WebSocketService - Real-time communication for messenger widget
4911
+ * HomeView - Welcome screen with team info and quick actions
4848
4912
  */
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);
4913
+ class HomeView {
4914
+ constructor(state, options = {}) {
4915
+ this.state = state;
4916
+ this.options = options;
4917
+ this.element = null;
4918
+ this._unsubscribe = null;
4872
4919
  }
4873
4920
 
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
- }
4921
+ render() {
4922
+ this.element = document.createElement('div');
4923
+ this.element.className = 'messenger-view messenger-home-view';
4886
4924
 
4887
- // Mock mode - simulate connection
4888
- if (this.mock) {
4889
- this.isConnected = true;
4890
- this._emit('connected', {});
4891
- this._startMockResponses();
4892
- return;
4893
- }
4925
+ this._updateContent();
4894
4926
 
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)}`;
4927
+ // Subscribe to state changes to re-render when data loads
4928
+ this._unsubscribe = this.state.subscribe((type) => {
4929
+ if (
4930
+ type === 'homeChangelogUpdate' ||
4931
+ type === 'conversationsUpdate' ||
4932
+ type === 'availabilityUpdate'
4933
+ ) {
4934
+ this._updateContent();
4935
+ }
4936
+ });
4900
4937
 
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
- }
4938
+ return this.element;
4911
4939
  }
4912
4940
 
4913
- /**
4914
- * Disconnect from WebSocket server
4915
- */
4916
- disconnect() {
4917
- this.isConnected = false;
4918
- this.reconnectAttempts = this.maxReconnectAttempts; // Prevent reconnection
4941
+ _updateContent() {
4942
+ const avatarsHtml = this._renderAvatarStack();
4943
+ const recentChangelogHtml = this._renderRecentChangelog();
4919
4944
 
4920
- if (this.pingInterval) {
4921
- clearInterval(this.pingInterval);
4922
- this.pingInterval = null;
4923
- }
4945
+ this.element.innerHTML = `
4946
+ <div class="messenger-home-header">
4947
+ <div class="messenger-home-header-top">
4948
+ <div class="messenger-home-logo">
4949
+ ${this.options.logoUrl ? `<img src="${this.options.logoUrl}" alt="${this.state.teamName}" />` : ''}
4950
+ </div>
4951
+ <div class="messenger-home-avatars">${avatarsHtml}</div>
4952
+ <button class="messenger-close-btn" aria-label="Close">
4953
+ <i class="ph ph-x" style="font-size: 20px;"></i>
4954
+ </button>
4955
+ </div>
4956
+ <div class="messenger-home-welcome">
4957
+ <span class="messenger-home-greeting">Hello there.</span>
4958
+ <span class="messenger-home-question">${this.state.welcomeMessage}</span>
4959
+ ${this._renderAvailabilityStatus()}
4960
+ </div>
4961
+ </div>
4924
4962
 
4925
- if (this.ws) {
4926
- this.ws.close();
4927
- this.ws = null;
4928
- }
4963
+ <div class="messenger-home-body">
4964
+ <button class="messenger-home-message-btn">
4965
+ <span>Send us a message</span>
4966
+ <i class="ph ph-arrow-right" style="font-size: 16px;"></i>
4967
+ </button>
4929
4968
 
4930
- if (this._mockInterval) {
4931
- clearInterval(this._mockInterval);
4932
- this._mockInterval = null;
4933
- }
4934
- }
4969
+ ${this._renderFeaturedCard()}
4935
4970
 
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
- }
4971
+ ${recentChangelogHtml}
4972
+ </div>
4973
+ `;
4949
4974
 
4950
- /**
4951
- * Remove event listener
4952
- */
4953
- off(event, callback) {
4954
- if (this._listeners.has(event)) {
4955
- this._listeners.get(event).delete(callback);
4956
- }
4975
+ this._attachEvents();
4957
4976
  }
4958
4977
 
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;
4978
+ _renderAvatarStack() {
4979
+ const avatars = this.state.teamAvatars;
4980
+ if (!avatars || avatars.length === 0) {
4981
+ // Default avatars with initials
4982
+ return `
4983
+ <div class="messenger-avatar-stack">
4984
+ <div class="messenger-avatar" style="background: #5856d6;">S</div>
4985
+ <div class="messenger-avatar" style="background: #007aff;">T</div>
4986
+ </div>
4987
+ `;
4966
4988
  }
4967
4989
 
4968
- if (this.mock) {
4969
- // Mock mode - just log
4970
- console.log('[WebSocket Mock] Sending:', type, payload);
4971
- return;
4972
- }
4990
+ const avatarItems = avatars
4991
+ .slice(0, 3)
4992
+ .map((avatar, i) => {
4993
+ if (typeof avatar === 'string' && avatar.startsWith('http')) {
4994
+ return `<img class="messenger-avatar" src="${avatar}" alt="Team member" style="z-index: ${3 - i};" />`;
4995
+ }
4996
+ return `<div class="messenger-avatar" style="background: ${this._getAvatarColor(i)}; z-index: ${3 - i};">${avatar.charAt(0).toUpperCase()}</div>`;
4997
+ })
4998
+ .join('');
4973
4999
 
4974
- if (this.ws && this.ws.readyState === WebSocket.OPEN) {
4975
- this.ws.send(JSON.stringify({ type, payload }));
4976
- }
5000
+ return `<div class="messenger-avatar-stack">${avatarItems}</div>`;
4977
5001
  }
4978
5002
 
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);
5003
+ _getAvatarColor(index) {
5004
+ const colors = ['#5856d6', '#007aff', '#34c759', '#ff9500', '#ff3b30'];
5005
+ return colors[index % colors.length];
4991
5006
  }
4992
5007
 
4993
- _onMessage(event) {
4994
- try {
4995
- const data = JSON.parse(event.data);
4996
- const { type, payload } = data;
5008
+ _renderAvailabilityStatus() {
5009
+ const isOnline = this.state.agentsOnline;
5010
+ const responseTime =
5011
+ this.state.responseTime || 'We typically reply within a few minutes';
4997
5012
 
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);
5013
+ if (isOnline) {
5014
+ return `
5015
+ <div class="messenger-home-availability">
5016
+ <span class="messenger-availability-dot messenger-availability-online"></span>
5017
+ <span class="messenger-availability-text">We're online now</span>
5018
+ </div>
5019
+ `;
5026
5020
  }
5027
- }
5028
5021
 
5029
- _onClose(event) {
5030
- console.log('[WebSocket] Disconnected:', event.code, event.reason);
5031
- this.isConnected = false;
5022
+ return `
5023
+ <div class="messenger-home-availability">
5024
+ <span class="messenger-availability-dot messenger-availability-away"></span>
5025
+ <span class="messenger-availability-text">${responseTime}</span>
5026
+ </div>
5027
+ `;
5028
+ }
5032
5029
 
5033
- if (this.pingInterval) {
5034
- clearInterval(this.pingInterval);
5035
- this.pingInterval = null;
5030
+ _renderFeaturedCard() {
5031
+ // Only show if there's featured content configured
5032
+ if (!this.options.featuredContent) {
5033
+ return '';
5036
5034
  }
5037
5035
 
5038
- this._emit('disconnected', { code: event.code, reason: event.reason });
5039
- this._scheduleReconnect();
5040
- }
5036
+ const { title, description, imageUrl, action } =
5037
+ this.options.featuredContent;
5041
5038
 
5042
- _onError(error) {
5043
- console.error('[WebSocket] Error:', error);
5044
- this._emit('error', { error });
5039
+ return `
5040
+ <div class="messenger-home-featured">
5041
+ ${imageUrl ? `<img src="${imageUrl}" alt="${title}" class="messenger-home-featured-image" onerror="this.style.display='none';" />` : ''}
5042
+ <div class="messenger-home-featured-content">
5043
+ <h3>${title}</h3>
5044
+ <p>${description}</p>
5045
+ </div>
5046
+ ${action ? `<button class="messenger-home-featured-btn" data-action="${action.type}" data-value="${action.value}">${action.label}</button>` : ''}
5047
+ </div>
5048
+ `;
5045
5049
  }
5046
5050
 
5047
- _scheduleReconnect() {
5048
- if (this.reconnectAttempts >= this.maxReconnectAttempts) {
5049
- console.log('[WebSocket] Max reconnect attempts reached');
5050
- this._emit('reconnect_failed', {});
5051
- return;
5051
+ _renderRecentChangelog() {
5052
+ // Show recent changelog preview as cards with images
5053
+ const changelogItems = this.state.homeChangelogItems;
5054
+ if (changelogItems.length === 0) {
5055
+ return '';
5052
5056
  }
5053
5057
 
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})`);
5058
+ const changelogHtml = changelogItems
5059
+ .map(
5060
+ (item) => `
5061
+ <div class="messenger-home-changelog-card" data-changelog-id="${item.id}">
5062
+ ${
5063
+ item.coverImage
5064
+ ? `
5065
+ <div class="messenger-home-changelog-cover">
5066
+ <img src="${item.coverImage}" alt="${item.title}" onerror="this.style.display='none';" />
5067
+ ${item.coverText ? `<span class="messenger-home-changelog-cover-text">${item.coverText}</span>` : ''}
5068
+ </div>
5069
+ `
5070
+ : ''
5071
+ }
5072
+ <div class="messenger-home-changelog-card-content">
5073
+ <h4 class="messenger-home-changelog-card-title">${item.title}</h4>
5074
+ <p class="messenger-home-changelog-card-desc">${item.description || ''}</p>
5075
+ </div>
5076
+ </div>
5077
+ `
5078
+ )
5079
+ .join('');
5057
5080
 
5058
- setTimeout(() => {
5059
- this.connect();
5060
- }, delay);
5081
+ return `
5082
+ <div class="messenger-home-changelog-section">
5083
+ ${changelogHtml}
5084
+ </div>
5085
+ `;
5061
5086
  }
5062
5087
 
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
- }
5088
+ _formatDate(dateString) {
5089
+ if (!dateString) return '';
5090
+ const date = new Date(dateString);
5091
+ const now = new Date();
5092
+ const diffDays = Math.floor((now - date) / (1000 * 60 * 60 * 24));
5093
+
5094
+ if (diffDays === 0) return 'Today';
5095
+ if (diffDays === 1) return 'Yesterday';
5096
+ if (diffDays < 7) return `${diffDays}d ago`;
5097
+ return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
5073
5098
  }
5074
5099
 
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,
5100
+ _attachEvents() {
5101
+ // Close button
5102
+ this.element
5103
+ .querySelector('.messenger-close-btn')
5104
+ .addEventListener('click', () => {
5105
+ this.state.setOpen(false);
5106
+ });
5107
+
5108
+ // Send message button
5109
+ this.element
5110
+ .querySelector('.messenger-home-message-btn')
5111
+ .addEventListener('click', () => {
5112
+ this.state.setView('messages');
5113
+ });
5114
+
5115
+ // Changelog items
5116
+ this.element
5117
+ .querySelectorAll('.messenger-home-changelog-item')
5118
+ .forEach((item) => {
5119
+ item.addEventListener('click', () => {
5120
+ // Navigate to changelog view with specific item selected
5121
+ this.state.setView('changelog');
5087
5122
  });
5123
+ });
5088
5124
 
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
- }
5125
+ // See all changelog
5126
+ const seeAllBtn = this.element.querySelector(
5127
+ '.messenger-home-changelog-all'
5128
+ );
5129
+ if (seeAllBtn) {
5130
+ seeAllBtn.addEventListener('click', () => {
5131
+ this.state.setView('changelog');
5132
+ });
5133
+ }
5099
5134
 
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
- },
5135
+ // Featured card action
5136
+ const featuredBtn = this.element.querySelector(
5137
+ '.messenger-home-featured-btn'
5138
+ );
5139
+ if (featuredBtn) {
5140
+ featuredBtn.addEventListener('click', () => {
5141
+ const action = featuredBtn.dataset.action;
5142
+ const value = featuredBtn.dataset.value;
5143
+ if (action === 'url') {
5144
+ window.open(value, '_blank');
5145
+ } else if (action === 'view') {
5146
+ this.state.setView(value);
5147
+ }
5115
5148
  });
5116
5149
  }
5117
5150
  }
5151
+
5152
+ destroy() {
5153
+ if (this._unsubscribe) {
5154
+ this._unsubscribe();
5155
+ }
5156
+ if (this.element && this.element.parentNode) {
5157
+ this.element.parentNode.removeChild(this.element);
5158
+ }
5159
+ }
5118
5160
  }
5119
5161
 
5120
5162
  /**
@@ -5325,7 +5367,10 @@
5325
5367
  this.messengerState.addMessage(conversation_id, localMessage);
5326
5368
 
5327
5369
  // Update unread count if panel is closed or viewing different conversation
5328
- if (!this.messengerState.isOpen || this.messengerState.activeConversationId !== conversation_id) {
5370
+ if (
5371
+ !this.messengerState.isOpen ||
5372
+ this.messengerState.activeConversationId !== conversation_id
5373
+ ) {
5329
5374
  this._updateUnreadCount();
5330
5375
  }
5331
5376
  }
@@ -5359,7 +5404,9 @@
5359
5404
  const response = await this.apiService.getUnreadCount();
5360
5405
  if (response.status && response.data) {
5361
5406
  this.messengerState.unreadCount = response.data.unread_count || 0;
5362
- this.messengerState._notify('unreadCountChange', { count: this.messengerState.unreadCount });
5407
+ this.messengerState._notify('unreadCountChange', {
5408
+ count: this.messengerState.unreadCount,
5409
+ });
5363
5410
  }
5364
5411
  } catch (error) {
5365
5412
  console.error('[MessengerWidget] Failed to get unread count:', error);
@@ -5533,9 +5580,16 @@
5533
5580
  // Transform API response to local format
5534
5581
  return response.data.map((conv) => ({
5535
5582
  id: conv.id,
5536
- title: conv.subject || `Chat with ${conv.assigned_user?.name || 'Support'}`,
5583
+ title:
5584
+ conv.subject ||
5585
+ `Chat with ${conv.assigned_user?.name || 'Support'}`,
5537
5586
  participants: conv.assigned_user
5538
- ? [{ name: conv.assigned_user.name, avatarUrl: conv.assigned_user.avatar }]
5587
+ ? [
5588
+ {
5589
+ name: conv.assigned_user.name,
5590
+ avatarUrl: conv.assigned_user.avatar,
5591
+ },
5592
+ ]
5539
5593
  : [{ name: 'Support', avatarUrl: null }],
5540
5594
  lastMessage: conv.preview || conv.snippet || '',
5541
5595
  lastMessageTime: conv.last_message_at,
@@ -5559,7 +5613,8 @@
5559
5613
  id: collection.id,
5560
5614
  title: collection.title || collection.name,
5561
5615
  description: collection.description || '',
5562
- articleCount: collection.article_count || collection.articleCount || 0,
5616
+ articleCount:
5617
+ collection.article_count || collection.articleCount || 0,
5563
5618
  icon: collection.icon || 'ph-book-open',
5564
5619
  url: collection.url || `#/help/${collection.slug || collection.id}`,
5565
5620
  }));
@@ -5584,7 +5639,9 @@
5584
5639
  isOwn: msg.sender_type === 'customer',
5585
5640
  timestamp: msg.created_at,
5586
5641
  sender: {
5587
- name: msg.sender_name || (msg.sender_type === 'customer' ? 'You' : 'Support'),
5642
+ name:
5643
+ msg.sender_name ||
5644
+ (msg.sender_type === 'customer' ? 'You' : 'Support'),
5588
5645
  avatarUrl: msg.sender_avatar || null,
5589
5646
  },
5590
5647
  }));
@@ -6929,8 +6986,10 @@
6929
6986
  // Check backend tracking first (from init response)
6930
6987
  if (this.config.last_feedback_at) {
6931
6988
  try {
6932
- const backendTimestamp = new Date(this.config.last_feedback_at).getTime();
6933
- if ((now - backendTimestamp) < cooldownMs) {
6989
+ const backendTimestamp = new Date(
6990
+ this.config.last_feedback_at
6991
+ ).getTime();
6992
+ if (now - backendTimestamp < cooldownMs) {
6934
6993
  return true;
6935
6994
  }
6936
6995
  } catch (e) {
@@ -6945,7 +7004,7 @@
6945
7004
  if (!stored) return false;
6946
7005
 
6947
7006
  const data = JSON.parse(stored);
6948
- return (now - data.submittedAt) < cooldownMs;
7007
+ return now - data.submittedAt < cooldownMs;
6949
7008
  } catch (e) {
6950
7009
  return false;
6951
7010
  }