@product7/feedback-sdk 1.2.6 → 1.2.8

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;
@@ -2603,11 +2630,6 @@
2603
2630
  }
2604
2631
  </div>
2605
2632
  </div>
2606
- <div class="changelog-list-item-arrow">
2607
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
2608
- <path d="M9 18l6-6-6-6"/>
2609
- </svg>
2610
- </div>
2611
2633
  </div>
2612
2634
  `;
2613
2635
  }
@@ -2971,133 +2993,409 @@
2971
2993
  }
2972
2994
 
2973
2995
  /**
2974
- * MessengerState - State management for the Messenger widget
2996
+ * WebSocketService - Real-time communication for messenger widget
2975
2997
  */
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
2998
 
2987
- // Help articles
2988
- this.helpArticles = [];
2989
- this.helpSearchQuery = '';
2999
+ class WebSocketService {
3000
+ constructor(config = {}) {
3001
+ this.baseURL = config.baseURL || '';
3002
+ this.workspace = config.workspace || '';
3003
+ this.sessionToken = config.sessionToken || null;
3004
+ this.mock = config.mock || false;
2990
3005
 
2991
- // Changelog
2992
- this.homeChangelogItems = [];
2993
- this.changelogItems = [];
3006
+ this.ws = null;
3007
+ this.reconnectAttempts = 0;
3008
+ this.maxReconnectAttempts = 5;
3009
+ this.reconnectDelay = 1000;
3010
+ this.pingInterval = null;
3011
+ this.isConnected = false;
2994
3012
 
2995
- // Team info
2996
- this.teamName = options.teamName || 'Support';
2997
- this.teamAvatars = options.teamAvatars || [];
2998
- this.welcomeMessage = options.welcomeMessage || 'How can we help?';
3013
+ // Event listeners
3014
+ this._listeners = new Map();
2999
3015
 
3000
- // User info
3001
- this.userContext = options.userContext || null;
3016
+ // Bind methods
3017
+ this._onOpen = this._onOpen.bind(this);
3018
+ this._onMessage = this._onMessage.bind(this);
3019
+ this._onClose = this._onClose.bind(this);
3020
+ this._onError = this._onError.bind(this);
3021
+ }
3002
3022
 
3003
- // Feature flags
3004
- this.enableHelp = options.enableHelp !== false;
3005
- this.enableChangelog = options.enableChangelog !== false;
3023
+ /**
3024
+ * Connect to WebSocket server
3025
+ */
3026
+ connect(sessionToken = null) {
3027
+ if (sessionToken) {
3028
+ this.sessionToken = sessionToken;
3029
+ }
3006
3030
 
3007
- // Agent availability
3008
- this.agentsOnline = false;
3009
- this.onlineCount = 0;
3010
- this.responseTime = 'Usually replies within a few minutes';
3031
+ if (!this.sessionToken) {
3032
+ console.warn('[WebSocket] No session token provided');
3033
+ return;
3034
+ }
3011
3035
 
3012
- // Typing indicators
3013
- this.typingUsers = {}; // { conversationId: { userName, timestamp } }
3036
+ // Mock mode - simulate connection
3037
+ if (this.mock) {
3038
+ this.isConnected = true;
3039
+ this._emit('connected', {});
3040
+ this._startMockResponses();
3041
+ return;
3042
+ }
3014
3043
 
3015
- // Loading states
3016
- this.isLoading = false;
3017
- this.isLoadingMessages = false;
3044
+ // Build WebSocket URL
3045
+ const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
3046
+ let wsURL = this.baseURL.replace(/^https?:/, wsProtocol);
3047
+ wsURL = wsURL.replace('/api/v1', '');
3048
+ wsURL = `${wsURL}/api/v1/widget/messenger/ws?token=${encodeURIComponent(this.sessionToken)}`;
3018
3049
 
3019
- // Listeners
3020
- this._listeners = new Set();
3050
+ try {
3051
+ this.ws = new WebSocket(wsURL);
3052
+ this.ws.onopen = this._onOpen;
3053
+ this.ws.onmessage = this._onMessage;
3054
+ this.ws.onclose = this._onClose;
3055
+ this.ws.onerror = this._onError;
3056
+ } catch (error) {
3057
+ console.error('[WebSocket] Connection error:', error);
3058
+ this._scheduleReconnect();
3059
+ }
3021
3060
  }
3022
3061
 
3023
3062
  /**
3024
- * Subscribe to state changes
3063
+ * Disconnect from WebSocket server
3025
3064
  */
3026
- subscribe(callback) {
3027
- this._listeners.add(callback);
3028
- return () => this._listeners.delete(callback);
3029
- }
3065
+ disconnect() {
3066
+ this.isConnected = false;
3067
+ this.reconnectAttempts = this.maxReconnectAttempts; // Prevent reconnection
3030
3068
 
3031
- /**
3032
- * Notify all listeners of state change
3033
- */
3034
- _notify(changeType, data) {
3035
- this._listeners.forEach((cb) => cb(changeType, data, this));
3036
- }
3069
+ if (this.pingInterval) {
3070
+ clearInterval(this.pingInterval);
3071
+ this.pingInterval = null;
3072
+ }
3037
3073
 
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
- }
3074
+ if (this.ws) {
3075
+ this.ws.close();
3076
+ this.ws = null;
3077
+ }
3046
3078
 
3047
- /**
3048
- * Toggle panel open/closed
3049
- */
3050
- setOpen(isOpen) {
3051
- this.isOpen = isOpen;
3052
- this._notify('openChange', { isOpen });
3079
+ if (this._mockInterval) {
3080
+ clearInterval(this._mockInterval);
3081
+ this._mockInterval = null;
3082
+ }
3053
3083
  }
3054
3084
 
3055
3085
  /**
3056
- * Set active conversation for chat view
3086
+ * Subscribe to events
3087
+ * @param {string} event - Event name
3088
+ * @param {Function} callback - Event handler
3089
+ * @returns {Function} Unsubscribe function
3057
3090
  */
3058
- setActiveConversation(conversationId) {
3059
- this.activeConversationId = conversationId;
3060
- this._notify('conversationChange', { conversationId });
3091
+ on(event, callback) {
3092
+ if (!this._listeners.has(event)) {
3093
+ this._listeners.set(event, new Set());
3094
+ }
3095
+ this._listeners.get(event).add(callback);
3096
+ return () => this._listeners.get(event).delete(callback);
3061
3097
  }
3062
3098
 
3063
3099
  /**
3064
- * Update conversations list
3100
+ * Remove event listener
3065
3101
  */
3066
- setConversations(conversations) {
3067
- this.conversations = conversations;
3068
- this._updateUnreadCount();
3069
- this._notify('conversationsUpdate', { conversations });
3102
+ off(event, callback) {
3103
+ if (this._listeners.has(event)) {
3104
+ this._listeners.get(event).delete(callback);
3105
+ }
3070
3106
  }
3071
3107
 
3072
3108
  /**
3073
- * Add a new conversation
3109
+ * Send message through WebSocket
3074
3110
  */
3075
- addConversation(conversation) {
3076
- this.conversations.unshift(conversation);
3077
- this._updateUnreadCount();
3078
- this._notify('conversationAdded', { conversation });
3079
- }
3111
+ send(type, payload = {}) {
3112
+ if (!this.isConnected) {
3113
+ console.warn('[WebSocket] Not connected, cannot send message');
3114
+ return;
3115
+ }
3080
3116
 
3081
- /**
3082
- * Update messages for a conversation
3083
- */
3084
- setMessages(conversationId, messages) {
3085
- this.messages[conversationId] = messages;
3086
- this._notify('messagesUpdate', { conversationId, messages });
3087
- }
3117
+ if (this.mock) {
3118
+ // Mock mode - just log
3119
+ console.log('[WebSocket Mock] Sending:', type, payload);
3120
+ return;
3121
+ }
3088
3122
 
3089
- /**
3090
- * Add a message to a conversation
3091
- */
3092
- addMessage(conversationId, message) {
3093
- if (!this.messages[conversationId]) {
3094
- this.messages[conversationId] = [];
3123
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
3124
+ this.ws.send(JSON.stringify({ type, payload }));
3095
3125
  }
3096
- this.messages[conversationId].push(message);
3126
+ }
3097
3127
 
3098
- // Update conversation preview
3099
- const conv = this.conversations.find((c) => c.id === conversationId);
3100
- if (conv) {
3128
+ // Private methods
3129
+
3130
+ _onOpen() {
3131
+ console.log('[WebSocket] Connected');
3132
+ this.isConnected = true;
3133
+ this.reconnectAttempts = 0;
3134
+ this._emit('connected', {});
3135
+
3136
+ // Start ping interval to keep connection alive
3137
+ this.pingInterval = setInterval(() => {
3138
+ this.send('ping', {});
3139
+ }, 30000);
3140
+ }
3141
+
3142
+ _onMessage(event) {
3143
+ try {
3144
+ const data = JSON.parse(event.data);
3145
+ const { type, payload } = data;
3146
+
3147
+ // Handle different event types
3148
+ switch (type) {
3149
+ case 'message:new':
3150
+ this._emit('message', payload);
3151
+ break;
3152
+ case 'typing:started':
3153
+ this._emit('typing_started', payload);
3154
+ break;
3155
+ case 'typing:stopped':
3156
+ this._emit('typing_stopped', payload);
3157
+ break;
3158
+ case 'conversation:updated':
3159
+ this._emit('conversation_updated', payload);
3160
+ break;
3161
+ case 'conversation:closed':
3162
+ this._emit('conversation_closed', payload);
3163
+ break;
3164
+ case 'availability:changed':
3165
+ this._emit('availability_changed', payload);
3166
+ break;
3167
+ case 'pong':
3168
+ // Ping response, ignore
3169
+ break;
3170
+ default:
3171
+ console.log('[WebSocket] Unknown event:', type, payload);
3172
+ }
3173
+ } catch (error) {
3174
+ console.error('[WebSocket] Failed to parse message:', error);
3175
+ }
3176
+ }
3177
+
3178
+ _onClose(event) {
3179
+ console.log('[WebSocket] Disconnected:', event.code, event.reason);
3180
+ this.isConnected = false;
3181
+
3182
+ if (this.pingInterval) {
3183
+ clearInterval(this.pingInterval);
3184
+ this.pingInterval = null;
3185
+ }
3186
+
3187
+ this._emit('disconnected', { code: event.code, reason: event.reason });
3188
+ this._scheduleReconnect();
3189
+ }
3190
+
3191
+ _onError(error) {
3192
+ console.error('[WebSocket] Error:', error);
3193
+ this._emit('error', { error });
3194
+ }
3195
+
3196
+ _scheduleReconnect() {
3197
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) {
3198
+ console.log('[WebSocket] Max reconnect attempts reached');
3199
+ this._emit('reconnect_failed', {});
3200
+ return;
3201
+ }
3202
+
3203
+ this.reconnectAttempts++;
3204
+ const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
3205
+ console.log(
3206
+ `[WebSocket] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`
3207
+ );
3208
+
3209
+ setTimeout(() => {
3210
+ this.connect();
3211
+ }, delay);
3212
+ }
3213
+
3214
+ _emit(event, data) {
3215
+ if (this._listeners.has(event)) {
3216
+ this._listeners.get(event).forEach((callback) => {
3217
+ try {
3218
+ callback(data);
3219
+ } catch (error) {
3220
+ console.error(`[WebSocket] Error in ${event} handler:`, error);
3221
+ }
3222
+ });
3223
+ }
3224
+ }
3225
+
3226
+ // Mock support for development
3227
+ _startMockResponses() {
3228
+ // Simulate agent typing and responses
3229
+ this._mockInterval = setInterval(() => {
3230
+ // Randomly emit typing or message events for demo
3231
+ const random = Math.random();
3232
+ if (random < 0.1) {
3233
+ this._emit('typing_started', {
3234
+ conversation_id: 'conv_1',
3235
+ user_id: 'agent_1',
3236
+ user_name: 'Sarah',
3237
+ is_agent: true,
3238
+ });
3239
+
3240
+ // Stop typing after 2 seconds
3241
+ setTimeout(() => {
3242
+ this._emit('typing_stopped', {
3243
+ conversation_id: 'conv_1',
3244
+ user_id: 'agent_1',
3245
+ });
3246
+ }, 2000);
3247
+ }
3248
+ }, 10000);
3249
+ }
3250
+
3251
+ /**
3252
+ * Simulate receiving a message (for mock mode)
3253
+ */
3254
+ simulateMessage(conversationId, message) {
3255
+ if (this.mock) {
3256
+ this._emit('message', {
3257
+ conversation_id: conversationId,
3258
+ message: {
3259
+ id: 'msg_' + Date.now(),
3260
+ content: message.content,
3261
+ sender_type: message.sender_type || 'agent',
3262
+ sender_name: message.sender_name || 'Support',
3263
+ sender_avatar: message.sender_avatar || null,
3264
+ created_at: new Date().toISOString(),
3265
+ },
3266
+ });
3267
+ }
3268
+ }
3269
+ }
3270
+
3271
+ /**
3272
+ * MessengerState - State management for the Messenger widget
3273
+ */
3274
+ class MessengerState {
3275
+ constructor(options = {}) {
3276
+ this.currentView = 'home'; // 'home', 'messages', 'chat', 'help', 'changelog'
3277
+ this.isOpen = false;
3278
+ this.unreadCount = 0;
3279
+ this.activeConversationId = null;
3280
+
3281
+ // Conversations
3282
+ this.conversations = [];
3283
+ this.messages = {}; // { conversationId: [messages] }
3284
+
3285
+ // Help articles
3286
+ this.helpArticles = [];
3287
+ this.helpSearchQuery = '';
3288
+
3289
+ // Changelog
3290
+ this.homeChangelogItems = [];
3291
+ this.changelogItems = [];
3292
+
3293
+ // Team info
3294
+ this.teamName = options.teamName || 'Support';
3295
+ this.teamAvatars = options.teamAvatars || [];
3296
+ this.welcomeMessage = options.welcomeMessage || 'How can we help?';
3297
+
3298
+ // User info
3299
+ this.userContext = options.userContext || null;
3300
+
3301
+ // Feature flags
3302
+ this.enableHelp = options.enableHelp !== false;
3303
+ this.enableChangelog = options.enableChangelog !== false;
3304
+
3305
+ // Agent availability
3306
+ this.agentsOnline = false;
3307
+ this.onlineCount = 0;
3308
+ this.responseTime = 'Usually replies within a few minutes';
3309
+
3310
+ // Typing indicators
3311
+ this.typingUsers = {}; // { conversationId: { userName, timestamp } }
3312
+
3313
+ // Loading states
3314
+ this.isLoading = false;
3315
+ this.isLoadingMessages = false;
3316
+
3317
+ // Listeners
3318
+ this._listeners = new Set();
3319
+ }
3320
+
3321
+ /**
3322
+ * Subscribe to state changes
3323
+ */
3324
+ subscribe(callback) {
3325
+ this._listeners.add(callback);
3326
+ return () => this._listeners.delete(callback);
3327
+ }
3328
+
3329
+ /**
3330
+ * Notify all listeners of state change
3331
+ */
3332
+ _notify(changeType, data) {
3333
+ this._listeners.forEach((cb) => cb(changeType, data, this));
3334
+ }
3335
+
3336
+ /**
3337
+ * Set current view
3338
+ */
3339
+ setView(view) {
3340
+ const previousView = this.currentView;
3341
+ this.currentView = view;
3342
+ this._notify('viewChange', { previousView, currentView: view });
3343
+ }
3344
+
3345
+ /**
3346
+ * Toggle panel open/closed
3347
+ */
3348
+ setOpen(isOpen) {
3349
+ this.isOpen = isOpen;
3350
+ this._notify('openChange', { isOpen });
3351
+ }
3352
+
3353
+ /**
3354
+ * Set active conversation for chat view
3355
+ */
3356
+ setActiveConversation(conversationId) {
3357
+ this.activeConversationId = conversationId;
3358
+ this._notify('conversationChange', { conversationId });
3359
+ }
3360
+
3361
+ /**
3362
+ * Update conversations list
3363
+ */
3364
+ setConversations(conversations) {
3365
+ this.conversations = conversations;
3366
+ this._updateUnreadCount();
3367
+ this._notify('conversationsUpdate', { conversations });
3368
+ }
3369
+
3370
+ /**
3371
+ * Add a new conversation
3372
+ */
3373
+ addConversation(conversation) {
3374
+ this.conversations.unshift(conversation);
3375
+ this._updateUnreadCount();
3376
+ this._notify('conversationAdded', { conversation });
3377
+ }
3378
+
3379
+ /**
3380
+ * Update messages for a conversation
3381
+ */
3382
+ setMessages(conversationId, messages) {
3383
+ this.messages[conversationId] = messages;
3384
+ this._notify('messagesUpdate', { conversationId, messages });
3385
+ }
3386
+
3387
+ /**
3388
+ * Add a message to a conversation
3389
+ */
3390
+ addMessage(conversationId, message) {
3391
+ if (!this.messages[conversationId]) {
3392
+ this.messages[conversationId] = [];
3393
+ }
3394
+ this.messages[conversationId].push(message);
3395
+
3396
+ // Update conversation preview
3397
+ const conv = this.conversations.find((c) => c.id === conversationId);
3398
+ if (conv) {
3101
3399
  conv.lastMessage = message.content;
3102
3400
  conv.lastMessageTime = message.timestamp;
3103
3401
  if (!message.isOwn) {
@@ -3828,7 +4126,10 @@
3828
4126
  data.conversationId === this.state.activeConversationId
3829
4127
  ) {
3830
4128
  this._hideTypingIndicator();
3831
- } else if (type === 'messagesUpdate' && data.conversationId === this.state.activeConversationId) {
4129
+ } else if (
4130
+ type === 'messagesUpdate' &&
4131
+ data.conversationId === this.state.activeConversationId
4132
+ ) {
3832
4133
  this._updateContent();
3833
4134
  }
3834
4135
  });
@@ -3888,14 +4189,17 @@
3888
4189
  </div>
3889
4190
  `;
3890
4191
 
3891
- this._typingIndicator = this.element.querySelector('.messenger-typing-indicator');
4192
+ this._typingIndicator = this.element.querySelector(
4193
+ '.messenger-typing-indicator'
4194
+ );
3892
4195
  this._attachEvents();
3893
4196
  this._scrollToBottom();
3894
4197
  }
3895
4198
 
3896
4199
  _renderEmptyState(isNewConversation = false) {
3897
4200
  const avatarHtml = this._renderTeamAvatars();
3898
- const responseTime = this.state.responseTime || 'We typically reply within a few minutes';
4201
+ const responseTime =
4202
+ this.state.responseTime || 'We typically reply within a few minutes';
3899
4203
  const isOnline = this.state.agentsOnline;
3900
4204
 
3901
4205
  return `
@@ -4151,7 +4455,9 @@
4151
4455
  _showTypingIndicator(userName) {
4152
4456
  if (this._typingIndicator) {
4153
4457
  this._typingIndicator.style.display = 'flex';
4154
- const textEl = this._typingIndicator.querySelector('.messenger-typing-text');
4458
+ const textEl = this._typingIndicator.querySelector(
4459
+ '.messenger-typing-text'
4460
+ );
4155
4461
  if (textEl) {
4156
4462
  textEl.textContent = `${userName || 'Support'} is typing...`;
4157
4463
  }
@@ -4532,255 +4838,19 @@
4532
4838
  <p>Try a different search term</p>
4533
4839
  </div>
4534
4840
  `;
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('');
4841
+ }
4764
4842
 
4765
4843
  return `
4766
- <div class="messenger-home-changelog-section">
4767
- ${changelogHtml}
4844
+ <div class="messenger-help-empty">
4845
+ <div class="messenger-help-empty-icon">
4846
+ <i class="ph ph-question" style="font-size: 48px;"></i>
4847
+ </div>
4848
+ <h3>Help collections</h3>
4849
+ <p>No collections available yet</p>
4768
4850
  </div>
4769
4851
  `;
4770
4852
  }
4771
4853
 
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
4854
  _attachEvents() {
4785
4855
  // Close button
4786
4856
  this.element
@@ -4789,48 +4859,37 @@
4789
4859
  this.state.setOpen(false);
4790
4860
  });
4791
4861
 
4792
- // Send message button
4793
- this.element
4794
- .querySelector('.messenger-home-message-btn')
4795
- .addEventListener('click', () => {
4796
- this.state.setView('messages');
4797
- });
4862
+ // Search input
4863
+ const searchInput = this.element.querySelector(
4864
+ '.messenger-help-search-input'
4865
+ );
4866
+ let searchTimeout;
4867
+ searchInput.addEventListener('input', (e) => {
4868
+ clearTimeout(searchTimeout);
4869
+ searchTimeout = setTimeout(() => {
4870
+ this.state.setHelpSearchQuery(e.target.value);
4871
+ }, 300);
4872
+ });
4798
4873
 
4799
- // Changelog items
4874
+ this._attachCollectionEvents();
4875
+ }
4876
+
4877
+ _attachCollectionEvents() {
4800
4878
  this.element
4801
- .querySelectorAll('.messenger-home-changelog-item')
4879
+ .querySelectorAll('.messenger-help-collection')
4802
4880
  .forEach((item) => {
4803
4881
  item.addEventListener('click', () => {
4804
- // Navigate to changelog view with specific item selected
4805
- this.state.setView('changelog');
4882
+ const collectionId = item.dataset.collectionId;
4883
+ const collection = this.state.helpArticles.find(
4884
+ (c) => c.id === collectionId
4885
+ );
4886
+ if (collection && collection.url) {
4887
+ window.open(collection.url, '_blank');
4888
+ } else if (this.options.onArticleClick) {
4889
+ this.options.onArticleClick(collection);
4890
+ }
4806
4891
  });
4807
4892
  });
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
4893
  }
4835
4894
 
4836
4895
  destroy() {
@@ -4844,277 +4903,255 @@
4844
4903
  }
4845
4904
 
4846
4905
  /**
4847
- * WebSocketService - Real-time communication for messenger widget
4906
+ * HomeView - Welcome screen with team info and quick actions
4848
4907
  */
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);
4908
+ class HomeView {
4909
+ constructor(state, options = {}) {
4910
+ this.state = state;
4911
+ this.options = options;
4912
+ this.element = null;
4913
+ this._unsubscribe = null;
4872
4914
  }
4873
4915
 
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
- }
4916
+ render() {
4917
+ this.element = document.createElement('div');
4918
+ this.element.className = 'messenger-view messenger-home-view';
4886
4919
 
4887
- // Mock mode - simulate connection
4888
- if (this.mock) {
4889
- this.isConnected = true;
4890
- this._emit('connected', {});
4891
- this._startMockResponses();
4892
- return;
4893
- }
4920
+ this._updateContent();
4894
4921
 
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)}`;
4922
+ // Subscribe to state changes to re-render when data loads
4923
+ this._unsubscribe = this.state.subscribe((type) => {
4924
+ if (
4925
+ type === 'homeChangelogUpdate' ||
4926
+ type === 'conversationsUpdate' ||
4927
+ type === 'availabilityUpdate'
4928
+ ) {
4929
+ this._updateContent();
4930
+ }
4931
+ });
4900
4932
 
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
- }
4933
+ return this.element;
4911
4934
  }
4912
4935
 
4913
- /**
4914
- * Disconnect from WebSocket server
4915
- */
4916
- disconnect() {
4917
- this.isConnected = false;
4918
- this.reconnectAttempts = this.maxReconnectAttempts; // Prevent reconnection
4936
+ _updateContent() {
4937
+ const avatarsHtml = this._renderAvatarStack();
4938
+ const recentChangelogHtml = this._renderRecentChangelog();
4919
4939
 
4920
- if (this.pingInterval) {
4921
- clearInterval(this.pingInterval);
4922
- this.pingInterval = null;
4923
- }
4940
+ this.element.innerHTML = `
4941
+ <div class="messenger-home-header">
4942
+ <div class="messenger-home-header-top">
4943
+ <div class="messenger-home-logo">
4944
+ ${this.options.logoUrl ? `<img src="${this.options.logoUrl}" alt="${this.state.teamName}" />` : ''}
4945
+ </div>
4946
+ <div class="messenger-home-avatars">${avatarsHtml}</div>
4947
+ <button class="messenger-close-btn" aria-label="Close">
4948
+ <i class="ph ph-x" style="font-size: 20px;"></i>
4949
+ </button>
4950
+ </div>
4951
+ <div class="messenger-home-welcome">
4952
+ <span class="messenger-home-greeting">Hello there.</span>
4953
+ <span class="messenger-home-question">${this.state.welcomeMessage}</span>
4954
+ ${this._renderAvailabilityStatus()}
4955
+ </div>
4956
+ </div>
4924
4957
 
4925
- if (this.ws) {
4926
- this.ws.close();
4927
- this.ws = null;
4928
- }
4958
+ <div class="messenger-home-body">
4959
+ <button class="messenger-home-message-btn">
4960
+ <span>Send us a message</span>
4961
+ <i class="ph ph-arrow-right" style="font-size: 16px;"></i>
4962
+ </button>
4929
4963
 
4930
- if (this._mockInterval) {
4931
- clearInterval(this._mockInterval);
4932
- this._mockInterval = null;
4933
- }
4934
- }
4964
+ ${this._renderFeaturedCard()}
4935
4965
 
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
- }
4966
+ ${recentChangelogHtml}
4967
+ </div>
4968
+ `;
4949
4969
 
4950
- /**
4951
- * Remove event listener
4952
- */
4953
- off(event, callback) {
4954
- if (this._listeners.has(event)) {
4955
- this._listeners.get(event).delete(callback);
4956
- }
4970
+ this._attachEvents();
4957
4971
  }
4958
4972
 
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;
4973
+ _renderAvatarStack() {
4974
+ const avatars = this.state.teamAvatars;
4975
+ if (!avatars || avatars.length === 0) {
4976
+ // Default avatars with initials
4977
+ return `
4978
+ <div class="messenger-avatar-stack">
4979
+ <div class="messenger-avatar" style="background: #5856d6;">S</div>
4980
+ <div class="messenger-avatar" style="background: #007aff;">T</div>
4981
+ </div>
4982
+ `;
4966
4983
  }
4967
4984
 
4968
- if (this.mock) {
4969
- // Mock mode - just log
4970
- console.log('[WebSocket Mock] Sending:', type, payload);
4971
- return;
4972
- }
4985
+ const avatarItems = avatars
4986
+ .slice(0, 3)
4987
+ .map((avatar, i) => {
4988
+ if (typeof avatar === 'string' && avatar.startsWith('http')) {
4989
+ return `<img class="messenger-avatar" src="${avatar}" alt="Team member" style="z-index: ${3 - i};" />`;
4990
+ }
4991
+ return `<div class="messenger-avatar" style="background: ${this._getAvatarColor(i)}; z-index: ${3 - i};">${avatar.charAt(0).toUpperCase()}</div>`;
4992
+ })
4993
+ .join('');
4973
4994
 
4974
- if (this.ws && this.ws.readyState === WebSocket.OPEN) {
4975
- this.ws.send(JSON.stringify({ type, payload }));
4976
- }
4995
+ return `<div class="messenger-avatar-stack">${avatarItems}</div>`;
4977
4996
  }
4978
4997
 
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);
4998
+ _getAvatarColor(index) {
4999
+ const colors = ['#5856d6', '#007aff', '#34c759', '#ff9500', '#ff3b30'];
5000
+ return colors[index % colors.length];
4991
5001
  }
4992
5002
 
4993
- _onMessage(event) {
4994
- try {
4995
- const data = JSON.parse(event.data);
4996
- const { type, payload } = data;
5003
+ _renderAvailabilityStatus() {
5004
+ const isOnline = this.state.agentsOnline;
5005
+ const responseTime =
5006
+ this.state.responseTime || 'We typically reply within a few minutes';
4997
5007
 
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);
5008
+ if (isOnline) {
5009
+ return `
5010
+ <div class="messenger-home-availability">
5011
+ <span class="messenger-availability-dot messenger-availability-online"></span>
5012
+ <span class="messenger-availability-text">We're online now</span>
5013
+ </div>
5014
+ `;
5026
5015
  }
5027
- }
5028
5016
 
5029
- _onClose(event) {
5030
- console.log('[WebSocket] Disconnected:', event.code, event.reason);
5031
- this.isConnected = false;
5017
+ return `
5018
+ <div class="messenger-home-availability">
5019
+ <span class="messenger-availability-dot messenger-availability-away"></span>
5020
+ <span class="messenger-availability-text">${responseTime}</span>
5021
+ </div>
5022
+ `;
5023
+ }
5032
5024
 
5033
- if (this.pingInterval) {
5034
- clearInterval(this.pingInterval);
5035
- this.pingInterval = null;
5025
+ _renderFeaturedCard() {
5026
+ // Only show if there's featured content configured
5027
+ if (!this.options.featuredContent) {
5028
+ return '';
5036
5029
  }
5037
5030
 
5038
- this._emit('disconnected', { code: event.code, reason: event.reason });
5039
- this._scheduleReconnect();
5040
- }
5031
+ const { title, description, imageUrl, action } =
5032
+ this.options.featuredContent;
5041
5033
 
5042
- _onError(error) {
5043
- console.error('[WebSocket] Error:', error);
5044
- this._emit('error', { error });
5034
+ return `
5035
+ <div class="messenger-home-featured">
5036
+ ${imageUrl ? `<img src="${imageUrl}" alt="${title}" class="messenger-home-featured-image" onerror="this.style.display='none';" />` : ''}
5037
+ <div class="messenger-home-featured-content">
5038
+ <h3>${title}</h3>
5039
+ <p>${description}</p>
5040
+ </div>
5041
+ ${action ? `<button class="messenger-home-featured-btn" data-action="${action.type}" data-value="${action.value}">${action.label}</button>` : ''}
5042
+ </div>
5043
+ `;
5045
5044
  }
5046
5045
 
5047
- _scheduleReconnect() {
5048
- if (this.reconnectAttempts >= this.maxReconnectAttempts) {
5049
- console.log('[WebSocket] Max reconnect attempts reached');
5050
- this._emit('reconnect_failed', {});
5051
- return;
5046
+ _renderRecentChangelog() {
5047
+ // Show recent changelog preview as cards with images
5048
+ const changelogItems = this.state.homeChangelogItems;
5049
+ if (changelogItems.length === 0) {
5050
+ return '';
5052
5051
  }
5053
5052
 
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})`);
5053
+ const changelogHtml = changelogItems
5054
+ .map(
5055
+ (item) => `
5056
+ <div class="messenger-home-changelog-card" data-changelog-id="${item.id}">
5057
+ ${
5058
+ item.coverImage
5059
+ ? `
5060
+ <div class="messenger-home-changelog-cover">
5061
+ <img src="${item.coverImage}" alt="${item.title}" onerror="this.style.display='none';" />
5062
+ ${item.coverText ? `<span class="messenger-home-changelog-cover-text">${item.coverText}</span>` : ''}
5063
+ </div>
5064
+ `
5065
+ : ''
5066
+ }
5067
+ <div class="messenger-home-changelog-card-content">
5068
+ <h4 class="messenger-home-changelog-card-title">${item.title}</h4>
5069
+ <p class="messenger-home-changelog-card-desc">${item.description || ''}</p>
5070
+ </div>
5071
+ </div>
5072
+ `
5073
+ )
5074
+ .join('');
5057
5075
 
5058
- setTimeout(() => {
5059
- this.connect();
5060
- }, delay);
5076
+ return `
5077
+ <div class="messenger-home-changelog-section">
5078
+ ${changelogHtml}
5079
+ </div>
5080
+ `;
5061
5081
  }
5062
5082
 
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
- }
5083
+ _formatDate(dateString) {
5084
+ if (!dateString) return '';
5085
+ const date = new Date(dateString);
5086
+ const now = new Date();
5087
+ const diffDays = Math.floor((now - date) / (1000 * 60 * 60 * 24));
5088
+
5089
+ if (diffDays === 0) return 'Today';
5090
+ if (diffDays === 1) return 'Yesterday';
5091
+ if (diffDays < 7) return `${diffDays}d ago`;
5092
+ return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
5073
5093
  }
5074
5094
 
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,
5095
+ _attachEvents() {
5096
+ // Close button
5097
+ this.element
5098
+ .querySelector('.messenger-close-btn')
5099
+ .addEventListener('click', () => {
5100
+ this.state.setOpen(false);
5101
+ });
5102
+
5103
+ // Send message button
5104
+ this.element
5105
+ .querySelector('.messenger-home-message-btn')
5106
+ .addEventListener('click', () => {
5107
+ this.state.setView('messages');
5108
+ });
5109
+
5110
+ // Changelog items
5111
+ this.element
5112
+ .querySelectorAll('.messenger-home-changelog-item')
5113
+ .forEach((item) => {
5114
+ item.addEventListener('click', () => {
5115
+ // Navigate to changelog view with specific item selected
5116
+ this.state.setView('changelog');
5087
5117
  });
5118
+ });
5088
5119
 
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
- }
5120
+ // See all changelog
5121
+ const seeAllBtn = this.element.querySelector(
5122
+ '.messenger-home-changelog-all'
5123
+ );
5124
+ if (seeAllBtn) {
5125
+ seeAllBtn.addEventListener('click', () => {
5126
+ this.state.setView('changelog');
5127
+ });
5128
+ }
5099
5129
 
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
- },
5130
+ // Featured card action
5131
+ const featuredBtn = this.element.querySelector(
5132
+ '.messenger-home-featured-btn'
5133
+ );
5134
+ if (featuredBtn) {
5135
+ featuredBtn.addEventListener('click', () => {
5136
+ const action = featuredBtn.dataset.action;
5137
+ const value = featuredBtn.dataset.value;
5138
+ if (action === 'url') {
5139
+ window.open(value, '_blank');
5140
+ } else if (action === 'view') {
5141
+ this.state.setView(value);
5142
+ }
5115
5143
  });
5116
5144
  }
5117
5145
  }
5146
+
5147
+ destroy() {
5148
+ if (this._unsubscribe) {
5149
+ this._unsubscribe();
5150
+ }
5151
+ if (this.element && this.element.parentNode) {
5152
+ this.element.parentNode.removeChild(this.element);
5153
+ }
5154
+ }
5118
5155
  }
5119
5156
 
5120
5157
  /**
@@ -5325,7 +5362,10 @@
5325
5362
  this.messengerState.addMessage(conversation_id, localMessage);
5326
5363
 
5327
5364
  // Update unread count if panel is closed or viewing different conversation
5328
- if (!this.messengerState.isOpen || this.messengerState.activeConversationId !== conversation_id) {
5365
+ if (
5366
+ !this.messengerState.isOpen ||
5367
+ this.messengerState.activeConversationId !== conversation_id
5368
+ ) {
5329
5369
  this._updateUnreadCount();
5330
5370
  }
5331
5371
  }
@@ -5359,7 +5399,9 @@
5359
5399
  const response = await this.apiService.getUnreadCount();
5360
5400
  if (response.status && response.data) {
5361
5401
  this.messengerState.unreadCount = response.data.unread_count || 0;
5362
- this.messengerState._notify('unreadCountChange', { count: this.messengerState.unreadCount });
5402
+ this.messengerState._notify('unreadCountChange', {
5403
+ count: this.messengerState.unreadCount,
5404
+ });
5363
5405
  }
5364
5406
  } catch (error) {
5365
5407
  console.error('[MessengerWidget] Failed to get unread count:', error);
@@ -5533,9 +5575,16 @@
5533
5575
  // Transform API response to local format
5534
5576
  return response.data.map((conv) => ({
5535
5577
  id: conv.id,
5536
- title: conv.subject || `Chat with ${conv.assigned_user?.name || 'Support'}`,
5578
+ title:
5579
+ conv.subject ||
5580
+ `Chat with ${conv.assigned_user?.name || 'Support'}`,
5537
5581
  participants: conv.assigned_user
5538
- ? [{ name: conv.assigned_user.name, avatarUrl: conv.assigned_user.avatar }]
5582
+ ? [
5583
+ {
5584
+ name: conv.assigned_user.name,
5585
+ avatarUrl: conv.assigned_user.avatar,
5586
+ },
5587
+ ]
5539
5588
  : [{ name: 'Support', avatarUrl: null }],
5540
5589
  lastMessage: conv.preview || conv.snippet || '',
5541
5590
  lastMessageTime: conv.last_message_at,
@@ -5559,7 +5608,8 @@
5559
5608
  id: collection.id,
5560
5609
  title: collection.title || collection.name,
5561
5610
  description: collection.description || '',
5562
- articleCount: collection.article_count || collection.articleCount || 0,
5611
+ articleCount:
5612
+ collection.article_count || collection.articleCount || 0,
5563
5613
  icon: collection.icon || 'ph-book-open',
5564
5614
  url: collection.url || `#/help/${collection.slug || collection.id}`,
5565
5615
  }));
@@ -5584,7 +5634,9 @@
5584
5634
  isOwn: msg.sender_type === 'customer',
5585
5635
  timestamp: msg.created_at,
5586
5636
  sender: {
5587
- name: msg.sender_name || (msg.sender_type === 'customer' ? 'You' : 'Support'),
5637
+ name:
5638
+ msg.sender_name ||
5639
+ (msg.sender_type === 'customer' ? 'You' : 'Support'),
5588
5640
  avatarUrl: msg.sender_avatar || null,
5589
5641
  },
5590
5642
  }));
@@ -6929,8 +6981,10 @@
6929
6981
  // Check backend tracking first (from init response)
6930
6982
  if (this.config.last_feedback_at) {
6931
6983
  try {
6932
- const backendTimestamp = new Date(this.config.last_feedback_at).getTime();
6933
- if ((now - backendTimestamp) < cooldownMs) {
6984
+ const backendTimestamp = new Date(
6985
+ this.config.last_feedback_at
6986
+ ).getTime();
6987
+ if (now - backendTimestamp < cooldownMs) {
6934
6988
  return true;
6935
6989
  }
6936
6990
  } catch (e) {
@@ -6945,7 +6999,7 @@
6945
6999
  if (!stored) return false;
6946
7000
 
6947
7001
  const data = JSON.parse(stored);
6948
- return (now - data.submittedAt) < cooldownMs;
7002
+ return now - data.submittedAt < cooldownMs;
6949
7003
  } catch (e) {
6950
7004
  return false;
6951
7005
  }