@product7/feedback-sdk 1.3.0 → 1.3.2

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.
@@ -54,7 +54,7 @@
54
54
  },
55
55
  ];
56
56
 
57
- const MOCK_CONVERSATIONS = [
57
+ const MOCK_CONVERSATIONS$1 = [
58
58
  {
59
59
  id: 'conv_1',
60
60
  subject: 'Question about pricing',
@@ -85,7 +85,7 @@
85
85
  },
86
86
  ];
87
87
 
88
- const MOCK_MESSAGES = {
88
+ const MOCK_MESSAGES$1 = {
89
89
  conv_1: [
90
90
  {
91
91
  id: 'msg_1',
@@ -408,8 +408,8 @@
408
408
  await delay$1(300);
409
409
  return {
410
410
  status: true,
411
- data: MOCK_CONVERSATIONS,
412
- meta: { total: MOCK_CONVERSATIONS.length, page: 1, limit: 20 },
411
+ data: MOCK_CONVERSATIONS$1,
412
+ meta: { total: MOCK_CONVERSATIONS$1.length, page: 1, limit: 20 },
413
413
  };
414
414
  }
415
415
 
@@ -428,10 +428,10 @@
428
428
 
429
429
  if (this.api.mock) {
430
430
  await delay$1(200);
431
- const conv = MOCK_CONVERSATIONS.find((c) => c.id === conversationId);
431
+ const conv = MOCK_CONVERSATIONS$1.find((c) => c.id === conversationId);
432
432
  return {
433
433
  status: true,
434
- data: { ...conv, messages: MOCK_MESSAGES[conversationId] || [] },
434
+ data: { ...conv, messages: MOCK_MESSAGES$1[conversationId] || [] },
435
435
  };
436
436
  }
437
437
 
@@ -464,8 +464,8 @@
464
464
  },
465
465
  ],
466
466
  };
467
- MOCK_CONVERSATIONS.unshift(newConv);
468
- MOCK_MESSAGES[newConv.id] = newConv.messages;
467
+ MOCK_CONVERSATIONS$1.unshift(newConv);
468
+ MOCK_MESSAGES$1[newConv.id] = newConv.messages;
469
469
  return { status: true, data: newConv };
470
470
  }
471
471
 
@@ -493,10 +493,10 @@
493
493
  sender_type: 'customer',
494
494
  created_at: new Date().toISOString(),
495
495
  };
496
- if (!MOCK_MESSAGES[conversationId]) {
497
- MOCK_MESSAGES[conversationId] = [];
496
+ if (!MOCK_MESSAGES$1[conversationId]) {
497
+ MOCK_MESSAGES$1[conversationId] = [];
498
498
  }
499
- MOCK_MESSAGES[conversationId].push(newMessage);
499
+ MOCK_MESSAGES$1[conversationId].push(newMessage);
500
500
  return { status: true, data: newMessage };
501
501
  }
502
502
 
@@ -517,7 +517,7 @@
517
517
  await this.api._ensureSession();
518
518
 
519
519
  if (this.api.mock) {
520
- const count = MOCK_CONVERSATIONS.reduce(
520
+ const count = MOCK_CONVERSATIONS$1.reduce(
521
521
  (sum, c) => sum + (c.unread || 0),
522
522
  0
523
523
  );
@@ -562,7 +562,7 @@
562
562
  }
563
563
  }
564
564
 
565
- class APIError extends Error {
565
+ let APIError$1 = class APIError extends Error {
566
566
  constructor(status, message, response) {
567
567
  super(message);
568
568
  this.name = 'APIError';
@@ -585,7 +585,7 @@
585
585
  isServerError() {
586
586
  return this.status >= 500 && this.status < 600;
587
587
  }
588
- }
588
+ };
589
589
 
590
590
  class WidgetError extends Error {
591
591
  constructor(message, widgetType, widgetId) {
@@ -661,7 +661,7 @@
661
661
  }
662
662
 
663
663
  async submitSurveyResponse(surveyId, responseData) {
664
- if (!surveyId) throw new APIError(400, 'Survey ID is required');
664
+ if (!surveyId) throw new APIError$1(400, 'Survey ID is required');
665
665
 
666
666
  await this.api._ensureSession();
667
667
 
@@ -700,7 +700,7 @@
700
700
  }
701
701
 
702
702
  async dismissSurvey(surveyId) {
703
- if (!surveyId) throw new APIError(400, 'Survey ID is required');
703
+ if (!surveyId) throw new APIError$1(400, 'Survey ID is required');
704
704
 
705
705
  await this.api._ensureSession();
706
706
 
@@ -764,7 +764,7 @@
764
764
  }
765
765
 
766
766
  if (!this.workspace || !this.userContext) {
767
- throw new APIError(
767
+ throw new APIError$1(
768
768
  400,
769
769
  `Missing ${!this.workspace ? 'workspace' : 'user context'} for initialization`
770
770
  );
@@ -818,7 +818,7 @@
818
818
  expiresIn: response.expires_in,
819
819
  };
820
820
  } catch (error) {
821
- throw new APIError(
821
+ throw new APIError$1(
822
822
  error.status || 500,
823
823
  `Failed to initialize widget: ${error.message}`,
824
824
  error.response
@@ -831,7 +831,7 @@
831
831
  await this.init();
832
832
  }
833
833
  if (!this.sessionToken) {
834
- throw new APIError(401, 'No valid session token available');
834
+ throw new APIError$1(401, 'No valid session token available');
835
835
  }
836
836
  }
837
837
 
@@ -931,7 +931,7 @@
931
931
  errorMessage = (await response.text()) || errorMessage;
932
932
  }
933
933
 
934
- throw new APIError(response.status, errorMessage, responseData);
934
+ throw new APIError$1(response.status, errorMessage, responseData);
935
935
  }
936
936
 
937
937
  const contentType = response.headers.get('content-type');
@@ -941,8 +941,8 @@
941
941
 
942
942
  return await response.text();
943
943
  } catch (error) {
944
- if (error instanceof APIError) throw error;
945
- throw new APIError(0, error.message, null);
944
+ if (error instanceof APIError$1) throw error;
945
+ throw new APIError$1(0, error.message, null);
946
946
  }
947
947
  }
948
948
 
@@ -997,7 +997,29 @@
997
997
  }
998
998
 
999
999
  async checkAgentsOnline() {
1000
- return this.messenger.checkAgentsOnline();
1000
+ if (!this.isSessionValid()) {
1001
+ await this.init();
1002
+ }
1003
+
1004
+ if (this.mock) {
1005
+ return {
1006
+ status: true,
1007
+ data: {
1008
+ agents_online: true,
1009
+ online_count: 2,
1010
+ response_time: 'Usually replies within a few minutes',
1011
+ available_agents: [
1012
+ { full_name: 'Sarah', picture: '' },
1013
+ { full_name: 'Tom', picture: '' },
1014
+ ],
1015
+ },
1016
+ };
1017
+ }
1018
+
1019
+ return this._makeRequest('/widget/messenger/agents/online', {
1020
+ method: 'GET',
1021
+ headers: { Authorization: `Bearer ${this.sessionToken}` },
1022
+ });
1001
1023
  }
1002
1024
 
1003
1025
  async getConversations(options) {
@@ -1013,11 +1035,119 @@
1013
1035
  }
1014
1036
 
1015
1037
  async startConversation(data) {
1016
- return this.messenger.startConversation(data);
1038
+ if (!this.isSessionValid()) {
1039
+ console.log('[APIService] startConversation: session invalid, calling init...');
1040
+ try {
1041
+ await this.init();
1042
+ console.log('[APIService] startConversation: init result, token:', this.sessionToken ? 'set' : 'null');
1043
+ } catch (initError) {
1044
+ console.error('[APIService] startConversation: init failed:', initError.message);
1045
+ throw initError;
1046
+ }
1047
+ }
1048
+
1049
+ if (!this.sessionToken) {
1050
+ console.error('[APIService] startConversation: no session token after init');
1051
+ throw new APIError(401, 'No valid session token available');
1052
+ }
1053
+
1054
+ console.log('[APIService] startConversation: sending to', `${this.baseURL}/widget/messenger/conversations`, 'mock:', this.mock);
1055
+
1056
+ if (this.mock) {
1057
+ await new Promise((resolve) => setTimeout(resolve, 300));
1058
+ const newConv = {
1059
+ id: 'conv_' + Date.now(),
1060
+ subject: data.subject || 'New conversation',
1061
+ status: 'open',
1062
+ last_message_at: new Date().toISOString(),
1063
+ created_at: new Date().toISOString(),
1064
+ messages: [
1065
+ {
1066
+ id: 'msg_' + Date.now(),
1067
+ content: data.message,
1068
+ sender_type: 'customer',
1069
+ created_at: new Date().toISOString(),
1070
+ },
1071
+ ],
1072
+ };
1073
+ MOCK_CONVERSATIONS.unshift(newConv);
1074
+ MOCK_MESSAGES[newConv.id] = newConv.messages;
1075
+ return { status: true, data: newConv };
1076
+ }
1077
+
1078
+ return this._makeRequest('/widget/messenger/conversations', {
1079
+ method: 'POST',
1080
+ headers: {
1081
+ 'Content-Type': 'application/json',
1082
+ Authorization: `Bearer ${this.sessionToken}`,
1083
+ },
1084
+ body: JSON.stringify({
1085
+ message: data.message,
1086
+ subject: data.subject || '',
1087
+ }),
1088
+ });
1017
1089
  }
1018
1090
 
1019
1091
  async sendMessage(conversationId, data) {
1020
- return this.messenger.sendMessage(conversationId, data);
1092
+ if (!this.isSessionValid()) {
1093
+ await this.init();
1094
+ }
1095
+
1096
+ if (this.mock) {
1097
+ await new Promise((resolve) => setTimeout(resolve, 200));
1098
+ const newMessage = {
1099
+ id: 'msg_' + Date.now(),
1100
+ content: data.content,
1101
+ attachments: data.attachments || [],
1102
+ sender_type: 'customer',
1103
+ created_at: new Date().toISOString(),
1104
+ };
1105
+ if (!MOCK_MESSAGES[conversationId]) {
1106
+ MOCK_MESSAGES[conversationId] = [];
1107
+ }
1108
+ MOCK_MESSAGES[conversationId].push(newMessage);
1109
+ return { status: true, data: newMessage };
1110
+ }
1111
+
1112
+ const payload = { content: data.content };
1113
+ if (data.attachments && data.attachments.length > 0) {
1114
+ payload.attachments = data.attachments;
1115
+ }
1116
+
1117
+ return this._makeRequest(`/widget/messenger/conversations/${conversationId}/messages`, {
1118
+ method: 'POST',
1119
+ headers: {
1120
+ 'Content-Type': 'application/json',
1121
+ Authorization: `Bearer ${this.sessionToken}`,
1122
+ },
1123
+ body: JSON.stringify(payload),
1124
+ });
1125
+ }
1126
+
1127
+ /**
1128
+ * Upload a file to CDN via widget endpoint
1129
+ * @param {string} base64Data - Base64 encoded file data (with or without data URI prefix)
1130
+ * @param {string} filename - Original filename
1131
+ * @returns {Promise<Object>} Response with url
1132
+ */
1133
+ async uploadFile(base64Data, filename) {
1134
+ if (!this.isSessionValid()) {
1135
+ await this.init();
1136
+ }
1137
+
1138
+ if (this.mock) {
1139
+ await new Promise((resolve) => setTimeout(resolve, 300));
1140
+ return { status: true, url: `https://mock-cdn.example.com/${filename}` };
1141
+ }
1142
+
1143
+ return this._makeRequest('/widget/messenger/upload', {
1144
+ method: 'POST',
1145
+ headers: {
1146
+ 'Content-Type': 'application/json',
1147
+ Authorization: `Bearer ${this.sessionToken}`,
1148
+ },
1149
+ body: JSON.stringify({ file: base64Data, filename }),
1150
+ });
1021
1151
  }
1022
1152
 
1023
1153
  async sendTypingIndicator(conversationId, isTyping) {
@@ -1048,8 +1178,64 @@
1048
1178
  return this.help.searchHelpArticles(query, options);
1049
1179
  }
1050
1180
 
1051
- async getChangelogs(options) {
1052
- return this.changelog.getChangelogs(options);
1181
+ _loadStoredSession() {
1182
+ if (typeof localStorage === 'undefined') return false;
1183
+
1184
+ try {
1185
+ const stored = localStorage.getItem('feedbackSDK_session');
1186
+ if (!stored) return false;
1187
+
1188
+ const sessionData = JSON.parse(stored);
1189
+
1190
+ // Invalidate mock tokens when not in mock mode (and vice versa)
1191
+ const isMockToken = sessionData.token && sessionData.token.startsWith('mock_');
1192
+ if (isMockToken !== this.mock) {
1193
+ localStorage.removeItem('feedbackSDK_session');
1194
+ return false;
1195
+ }
1196
+
1197
+ this.sessionToken = sessionData.token;
1198
+ this.sessionExpiry = new Date(sessionData.expiry);
1199
+
1200
+ return this.isSessionValid();
1201
+ } catch (error) {
1202
+ return false;
1203
+ }
1204
+ }
1205
+
1206
+ async _makeRequest(endpoint, options = {}) {
1207
+ const url = `${this.baseURL}${endpoint}`;
1208
+
1209
+ try {
1210
+ const response = await fetch(url, options);
1211
+
1212
+ if (!response.ok) {
1213
+ let errorMessage = `HTTP ${response.status}`;
1214
+ let responseData = null;
1215
+
1216
+ try {
1217
+ responseData = await response.json();
1218
+ errorMessage =
1219
+ responseData.message || responseData.error || errorMessage;
1220
+ } catch (e) {
1221
+ errorMessage = (await response.text()) || errorMessage;
1222
+ }
1223
+
1224
+ throw new APIError(response.status, errorMessage, responseData);
1225
+ }
1226
+
1227
+ const contentType = response.headers.get('content-type');
1228
+ if (contentType && contentType.includes('application/json')) {
1229
+ return await response.json();
1230
+ }
1231
+
1232
+ return await response.text();
1233
+ } catch (error) {
1234
+ if (error instanceof APIError) {
1235
+ throw error;
1236
+ }
1237
+ throw new APIError(0, error.message, null);
1238
+ }
1053
1239
  }
1054
1240
  }
1055
1241
 
@@ -2772,333 +2958,57 @@
2772
2958
  }
2773
2959
 
2774
2960
  /**
2775
- * WebSocketService - Real-time communication for messenger widget
2961
+ * MessengerState - State management for the Messenger widget
2776
2962
  */
2963
+ class MessengerState {
2964
+ constructor(options = {}) {
2965
+ this.currentView = 'home'; // 'home', 'messages', 'chat', 'help', 'changelog'
2966
+ this.isOpen = false;
2967
+ this.unreadCount = 0;
2968
+ this.activeConversationId = null;
2777
2969
 
2778
- class WebSocketService {
2779
- constructor(config = {}) {
2780
- this.baseURL = config.baseURL || '';
2781
- this.workspace = config.workspace || '';
2782
- this.sessionToken = config.sessionToken || null;
2783
- this.mock = config.mock || false;
2970
+ // Conversations
2971
+ this.conversations = [];
2972
+ this.messages = {}; // { conversationId: [messages] }
2784
2973
 
2785
- this.ws = null;
2786
- this.reconnectAttempts = 0;
2787
- this.maxReconnectAttempts = 5;
2788
- this.reconnectDelay = 1000;
2789
- this.pingInterval = null;
2790
- this.isConnected = false;
2974
+ // Help articles
2975
+ this.helpArticles = [];
2976
+ this.helpSearchQuery = '';
2791
2977
 
2792
- // Event listeners
2793
- this._listeners = new Map();
2978
+ // Changelog
2979
+ this.homeChangelogItems = [];
2980
+ this.changelogItems = [];
2794
2981
 
2795
- // Bind methods
2796
- this._onOpen = this._onOpen.bind(this);
2797
- this._onMessage = this._onMessage.bind(this);
2798
- this._onClose = this._onClose.bind(this);
2799
- this._onError = this._onError.bind(this);
2800
- }
2982
+ // Team info
2983
+ this.teamName = options.teamName || 'Support';
2984
+ this.teamAvatars = options.teamAvatars || [];
2985
+ this.welcomeMessage = options.welcomeMessage || 'How can we help?';
2801
2986
 
2802
- /**
2803
- * Connect to WebSocket server
2804
- */
2805
- connect(sessionToken = null) {
2806
- if (sessionToken) {
2807
- this.sessionToken = sessionToken;
2808
- }
2987
+ // User info
2988
+ this.userContext = options.userContext || null;
2809
2989
 
2810
- if (!this.sessionToken) {
2811
- console.warn('[WebSocket] No session token provided');
2812
- return;
2813
- }
2990
+ // Feature flags
2991
+ this.enableHelp = options.enableHelp !== false;
2992
+ this.enableChangelog = options.enableChangelog !== false;
2814
2993
 
2815
- // Mock mode - simulate connection
2816
- if (this.mock) {
2817
- this.isConnected = true;
2818
- this._emit('connected', {});
2819
- this._startMockResponses();
2820
- return;
2821
- }
2994
+ // Agent availability
2995
+ this.agentsOnline = false;
2996
+ this.onlineCount = 0;
2997
+ this.responseTime = 'Usually replies within a few minutes';
2822
2998
 
2823
- // Build WebSocket URL
2824
- const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
2825
- let wsURL = this.baseURL.replace(/^https?:/, wsProtocol);
2826
- wsURL = wsURL.replace('/api/v1', '');
2827
- wsURL = `${wsURL}/api/v1/widget/messenger/ws?token=${encodeURIComponent(this.sessionToken)}`;
2999
+ // Typing indicators
3000
+ this.typingUsers = {}; // { conversationId: { userName, timestamp } }
2828
3001
 
2829
- try {
2830
- this.ws = new WebSocket(wsURL);
2831
- this.ws.onopen = this._onOpen;
2832
- this.ws.onmessage = this._onMessage;
2833
- this.ws.onclose = this._onClose;
2834
- this.ws.onerror = this._onError;
2835
- } catch (error) {
2836
- console.error('[WebSocket] Connection error:', error);
2837
- this._scheduleReconnect();
2838
- }
3002
+ // Loading states
3003
+ this.isLoading = false;
3004
+ this.isLoadingMessages = false;
3005
+
3006
+ // Listeners
3007
+ this._listeners = new Set();
2839
3008
  }
2840
3009
 
2841
3010
  /**
2842
- * Disconnect from WebSocket server
2843
- */
2844
- disconnect() {
2845
- this.isConnected = false;
2846
- this.reconnectAttempts = this.maxReconnectAttempts; // Prevent reconnection
2847
-
2848
- if (this.pingInterval) {
2849
- clearInterval(this.pingInterval);
2850
- this.pingInterval = null;
2851
- }
2852
-
2853
- if (this.ws) {
2854
- this.ws.close();
2855
- this.ws = null;
2856
- }
2857
-
2858
- if (this._mockInterval) {
2859
- clearInterval(this._mockInterval);
2860
- this._mockInterval = null;
2861
- }
2862
- }
2863
-
2864
- /**
2865
- * Subscribe to events
2866
- * @param {string} event - Event name
2867
- * @param {Function} callback - Event handler
2868
- * @returns {Function} Unsubscribe function
2869
- */
2870
- on(event, callback) {
2871
- if (!this._listeners.has(event)) {
2872
- this._listeners.set(event, new Set());
2873
- }
2874
- this._listeners.get(event).add(callback);
2875
- return () => this._listeners.get(event).delete(callback);
2876
- }
2877
-
2878
- /**
2879
- * Remove event listener
2880
- */
2881
- off(event, callback) {
2882
- if (this._listeners.has(event)) {
2883
- this._listeners.get(event).delete(callback);
2884
- }
2885
- }
2886
-
2887
- /**
2888
- * Send message through WebSocket
2889
- */
2890
- send(type, payload = {}) {
2891
- if (!this.isConnected) {
2892
- console.warn('[WebSocket] Not connected, cannot send message');
2893
- return;
2894
- }
2895
-
2896
- if (this.mock) {
2897
- // Mock mode - just log
2898
- console.log('[WebSocket Mock] Sending:', type, payload);
2899
- return;
2900
- }
2901
-
2902
- if (this.ws && this.ws.readyState === WebSocket.OPEN) {
2903
- this.ws.send(JSON.stringify({ type, payload }));
2904
- }
2905
- }
2906
-
2907
- // Private methods
2908
-
2909
- _onOpen() {
2910
- console.log('[WebSocket] Connected');
2911
- this.isConnected = true;
2912
- this.reconnectAttempts = 0;
2913
- this._emit('connected', {});
2914
-
2915
- // Start ping interval to keep connection alive
2916
- this.pingInterval = setInterval(() => {
2917
- this.send('ping', {});
2918
- }, 30000);
2919
- }
2920
-
2921
- _onMessage(event) {
2922
- try {
2923
- const data = JSON.parse(event.data);
2924
- const { type, payload } = data;
2925
-
2926
- // Handle different event types
2927
- switch (type) {
2928
- case 'message:new':
2929
- this._emit('message', payload);
2930
- break;
2931
- case 'typing:started':
2932
- this._emit('typing_started', payload);
2933
- break;
2934
- case 'typing:stopped':
2935
- this._emit('typing_stopped', payload);
2936
- break;
2937
- case 'conversation:updated':
2938
- this._emit('conversation_updated', payload);
2939
- break;
2940
- case 'conversation:closed':
2941
- this._emit('conversation_closed', payload);
2942
- break;
2943
- case 'availability:changed':
2944
- this._emit('availability_changed', payload);
2945
- break;
2946
- case 'pong':
2947
- // Ping response, ignore
2948
- break;
2949
- default:
2950
- console.log('[WebSocket] Unknown event:', type, payload);
2951
- }
2952
- } catch (error) {
2953
- console.error('[WebSocket] Failed to parse message:', error);
2954
- }
2955
- }
2956
-
2957
- _onClose(event) {
2958
- console.log('[WebSocket] Disconnected:', event.code, event.reason);
2959
- this.isConnected = false;
2960
-
2961
- if (this.pingInterval) {
2962
- clearInterval(this.pingInterval);
2963
- this.pingInterval = null;
2964
- }
2965
-
2966
- this._emit('disconnected', { code: event.code, reason: event.reason });
2967
- this._scheduleReconnect();
2968
- }
2969
-
2970
- _onError(error) {
2971
- console.error('[WebSocket] Error:', error);
2972
- this._emit('error', { error });
2973
- }
2974
-
2975
- _scheduleReconnect() {
2976
- if (this.reconnectAttempts >= this.maxReconnectAttempts) {
2977
- console.log('[WebSocket] Max reconnect attempts reached');
2978
- this._emit('reconnect_failed', {});
2979
- return;
2980
- }
2981
-
2982
- this.reconnectAttempts++;
2983
- const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
2984
- console.log(
2985
- `[WebSocket] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`
2986
- );
2987
-
2988
- setTimeout(() => {
2989
- this.connect();
2990
- }, delay);
2991
- }
2992
-
2993
- _emit(event, data) {
2994
- if (this._listeners.has(event)) {
2995
- this._listeners.get(event).forEach((callback) => {
2996
- try {
2997
- callback(data);
2998
- } catch (error) {
2999
- console.error(`[WebSocket] Error in ${event} handler:`, error);
3000
- }
3001
- });
3002
- }
3003
- }
3004
-
3005
- // Mock support for development
3006
- _startMockResponses() {
3007
- // Simulate agent typing and responses
3008
- this._mockInterval = setInterval(() => {
3009
- // Randomly emit typing or message events for demo
3010
- const random = Math.random();
3011
- if (random < 0.1) {
3012
- this._emit('typing_started', {
3013
- conversation_id: 'conv_1',
3014
- user_id: 'agent_1',
3015
- user_name: 'Sarah',
3016
- is_agent: true,
3017
- });
3018
-
3019
- // Stop typing after 2 seconds
3020
- setTimeout(() => {
3021
- this._emit('typing_stopped', {
3022
- conversation_id: 'conv_1',
3023
- user_id: 'agent_1',
3024
- });
3025
- }, 2000);
3026
- }
3027
- }, 10000);
3028
- }
3029
-
3030
- /**
3031
- * Simulate receiving a message (for mock mode)
3032
- */
3033
- simulateMessage(conversationId, message) {
3034
- if (this.mock) {
3035
- this._emit('message', {
3036
- conversation_id: conversationId,
3037
- message: {
3038
- id: 'msg_' + Date.now(),
3039
- content: message.content,
3040
- sender_type: message.sender_type || 'agent',
3041
- sender_name: message.sender_name || 'Support',
3042
- sender_avatar: message.sender_avatar || null,
3043
- created_at: new Date().toISOString(),
3044
- },
3045
- });
3046
- }
3047
- }
3048
- }
3049
-
3050
- /**
3051
- * MessengerState - State management for the Messenger widget
3052
- */
3053
- class MessengerState {
3054
- constructor(options = {}) {
3055
- this.currentView = 'home'; // 'home', 'messages', 'chat', 'help', 'changelog'
3056
- this.isOpen = false;
3057
- this.unreadCount = 0;
3058
- this.activeConversationId = null;
3059
-
3060
- // Conversations
3061
- this.conversations = [];
3062
- this.messages = {}; // { conversationId: [messages] }
3063
-
3064
- // Help articles
3065
- this.helpArticles = [];
3066
- this.helpSearchQuery = '';
3067
-
3068
- // Changelog
3069
- this.homeChangelogItems = [];
3070
- this.changelogItems = [];
3071
-
3072
- // Team info
3073
- this.teamName = options.teamName || 'Support';
3074
- this.teamAvatars = options.teamAvatars || [];
3075
- this.welcomeMessage = options.welcomeMessage || 'How can we help?';
3076
-
3077
- // User info
3078
- this.userContext = options.userContext || null;
3079
-
3080
- // Feature flags
3081
- this.enableHelp = options.enableHelp !== false;
3082
- this.enableChangelog = options.enableChangelog !== false;
3083
-
3084
- // Agent availability
3085
- this.agentsOnline = false;
3086
- this.onlineCount = 0;
3087
- this.responseTime = 'Usually replies within a few minutes';
3088
-
3089
- // Typing indicators
3090
- this.typingUsers = {}; // { conversationId: { userName, timestamp } }
3091
-
3092
- // Loading states
3093
- this.isLoading = false;
3094
- this.isLoadingMessages = false;
3095
-
3096
- // Listeners
3097
- this._listeners = new Set();
3098
- }
3099
-
3100
- /**
3101
- * Subscribe to state changes
3011
+ * Subscribe to state changes
3102
3012
  */
3103
3013
  subscribe(callback) {
3104
3014
  this._listeners.add(callback);
@@ -3133,8 +3043,9 @@
3133
3043
  * Set active conversation for chat view
3134
3044
  */
3135
3045
  setActiveConversation(conversationId) {
3046
+ const previousConversationId = this.activeConversationId;
3136
3047
  this.activeConversationId = conversationId;
3137
- this._notify('conversationChange', { conversationId });
3048
+ this._notify('conversationChange', { conversationId, previousConversationId });
3138
3049
  }
3139
3050
 
3140
3051
  /**
@@ -3186,6 +3097,20 @@
3186
3097
  this._notify('messageAdded', { conversationId, message });
3187
3098
  }
3188
3099
 
3100
+ /**
3101
+ * Update a conversation by id
3102
+ */
3103
+ updateConversation(conversationId, updates) {
3104
+ const conv = this.conversations.find((c) => c.id === conversationId);
3105
+ if (!conv) {
3106
+ return null;
3107
+ }
3108
+
3109
+ Object.assign(conv, updates);
3110
+ this._notify('conversationUpdated', { conversationId, conversation: conv });
3111
+ return conv;
3112
+ }
3113
+
3189
3114
  /**
3190
3115
  * Mark conversation as read
3191
3116
  */
@@ -3255,6 +3180,21 @@
3255
3180
  return this.messages[this.activeConversationId] || [];
3256
3181
  }
3257
3182
 
3183
+ /**
3184
+ * Update team avatars from backend agent data.
3185
+ * Converts available_agents ({full_name, picture}) into avatar strings
3186
+ * the views already support (URL strings or initial strings).
3187
+ */
3188
+ setTeamAvatarsFromAgents(agents) {
3189
+ if (!agents || agents.length === 0) return;
3190
+
3191
+ this.teamAvatars = agents.map((agent) => {
3192
+ if (agent.picture) return agent.picture;
3193
+ return agent.full_name || '?';
3194
+ });
3195
+ this._notify('teamAvatarsUpdate', { teamAvatars: this.teamAvatars });
3196
+ }
3197
+
3258
3198
  /**
3259
3199
  * Get filtered help articles
3260
3200
  */
@@ -3887,6 +3827,9 @@
3887
3827
  this._typingTimeout = null;
3888
3828
  this._isTyping = false;
3889
3829
  this._typingIndicator = null;
3830
+ this._isConversationClosed = false;
3831
+ this._showEmailOverlayFlag = false;
3832
+ this._pendingAttachments = []; // { file, preview, type }
3890
3833
  }
3891
3834
 
3892
3835
  render() {
@@ -3914,10 +3857,12 @@
3914
3857
  ) {
3915
3858
  this._hideTypingIndicator();
3916
3859
  } else if (
3917
- type === 'messagesUpdate' &&
3860
+ type === 'conversationUpdated' &&
3918
3861
  data.conversationId === this.state.activeConversationId
3919
3862
  ) {
3920
3863
  this._updateContent();
3864
+ } else if (type === 'messagesUpdate' && data.conversationId === this.state.activeConversationId) {
3865
+ this._updateContent();
3921
3866
  }
3922
3867
  });
3923
3868
 
@@ -3928,6 +3873,8 @@
3928
3873
  const conversation = this.state.getActiveConversation();
3929
3874
  const messages = this.state.getActiveMessages();
3930
3875
  const isNewConversation = !this.state.activeConversationId;
3876
+ const isClosed = !isNewConversation && conversation?.status === 'closed';
3877
+ this._isConversationClosed = isClosed;
3931
3878
 
3932
3879
  const messagesHtml =
3933
3880
  messages.length === 0
@@ -3940,7 +3887,11 @@
3940
3887
  : conversation?.title || 'Chat with team';
3941
3888
  const placeholder = isNewConversation
3942
3889
  ? 'Start typing your message...'
3943
- : 'Write a message...';
3890
+ : isClosed
3891
+ ? 'Conversation closed'
3892
+ : 'Write a message...';
3893
+
3894
+ const existingName = this.state.userContext?.name || '';
3944
3895
 
3945
3896
  this.element.innerHTML = `
3946
3897
  <div class="messenger-chat-header">
@@ -3962,6 +3913,12 @@
3962
3913
 
3963
3914
  <div class="messenger-chat-messages">
3964
3915
  ${messagesHtml}
3916
+ ${isClosed ? `
3917
+ <div class="messenger-closed-banner">
3918
+ <i class="ph ph-check-circle" style="font-size: 18px;"></i>
3919
+ <span>This conversation has been resolved</span>
3920
+ </div>
3921
+ ` : ''}
3965
3922
  <div class="messenger-typing-indicator" style="display: none;">
3966
3923
  <div class="messenger-typing-dots">
3967
3924
  <span></span><span></span><span></span>
@@ -3970,7 +3927,13 @@
3970
3927
  </div>
3971
3928
  </div>
3972
3929
 
3930
+ ${isClosed ? '' : `
3931
+ <div class="messenger-compose-attachments-preview"></div>
3932
+
3973
3933
  <div class="messenger-chat-compose">
3934
+ <button class="messenger-compose-attach" aria-label="Attach file">
3935
+ <i class="ph ph-paperclip" style="font-size: 20px;"></i>
3936
+ </button>
3974
3937
  <div class="messenger-compose-input-wrapper">
3975
3938
  <textarea class="messenger-compose-input" placeholder="${placeholder}" rows="1"></textarea>
3976
3939
  </div>
@@ -3979,6 +3942,21 @@
3979
3942
  <path d="M227.32,28.68a16,16,0,0,0-15.66-4.08l-.15,0L19.57,82.84a16,16,0,0,0-2.49,29.8L102,154l41.3,84.87A15.86,15.86,0,0,0,157.74,248q.69,0,1.38-.06a15.88,15.88,0,0,0,14-11.51l58.2-191.94c0-.05,0-.1,0-.15A16,16,0,0,0,227.32,28.68ZM157.83,231.85l-.05.14L118.42,148.9l47.24-47.25a8,8,0,0,0-11.31-11.31L107.1,137.58,24,98.22l.14,0L216,40Z"></path>
3980
3943
  </svg>
3981
3944
  </button>
3945
+ <input type="file" class="messenger-compose-file-input" style="display:none;" multiple accept="image/*,.pdf,.doc,.docx,.xls,.xlsx,.txt,.zip" />
3946
+ </div>
3947
+ `}
3948
+
3949
+ <div class="messenger-email-overlay" style="display: none;">
3950
+ <div class="messenger-email-card">
3951
+ <h4>What is your email address?</h4>
3952
+ <p>Enter your email to know when we reply:</p>
3953
+ <input type="text" class="messenger-email-name" placeholder="Name (optional)" value="${this._escapeHtml(existingName)}" autocomplete="name" />
3954
+ <input type="email" class="messenger-email-input" placeholder="Enter your email address..." autocomplete="email" />
3955
+ <div class="messenger-email-actions">
3956
+ <button class="messenger-email-submit" disabled>Set my email</button>
3957
+ <button class="messenger-email-skip">Skip</button>
3958
+ </div>
3959
+ </div>
3982
3960
  </div>
3983
3961
  `;
3984
3962
 
@@ -3987,6 +3965,12 @@
3987
3965
  );
3988
3966
  this._attachEvents();
3989
3967
  this._scrollToBottom();
3968
+ this._renderAttachmentPreviews();
3969
+
3970
+ // Show email overlay after first message sent without email
3971
+ if (this._showEmailOverlayFlag) {
3972
+ this._showEmailOverlay();
3973
+ }
3990
3974
  }
3991
3975
 
3992
3976
  _renderEmptyState(isNewConversation = false) {
@@ -4008,19 +3992,37 @@
4008
3992
  `;
4009
3993
  }
4010
3994
 
3995
+ _renderMessageAttachments(attachments) {
3996
+ if (!attachments || attachments.length === 0) return '';
3997
+ return attachments.map((att) => {
3998
+ if (att.type === 'image') {
3999
+ return `<img class="messenger-message-image" src="${this._escapeHtml(att.url)}" alt="${this._escapeHtml(att.name || 'image')}" data-url="${this._escapeHtml(att.url)}" />`;
4000
+ }
4001
+ return `<a class="messenger-message-file" href="${this._escapeHtml(att.url)}" data-url="${this._escapeHtml(att.url)}" data-name="${this._escapeHtml(att.name || 'file')}">
4002
+ <i class="ph ph-file" style="font-size:16px;"></i>
4003
+ <span>${this._escapeHtml(att.name || 'file')}</span>
4004
+ <i class="ph ph-download-simple messenger-file-download-icon" style="font-size:14px;"></i>
4005
+ </a>`;
4006
+ }).join('');
4007
+ }
4008
+
4011
4009
  _renderMessage(message) {
4012
4010
  const isOwn = message.isOwn;
4013
4011
  const messageClass = isOwn
4014
4012
  ? 'messenger-message-own'
4015
4013
  : 'messenger-message-received';
4016
4014
  const timeStr = this._formatMessageTime(message.timestamp);
4015
+ const attachmentsHtml = this._renderMessageAttachments(message.attachments);
4016
+
4017
+ const contentHtml = message.content ? `<div class="messenger-message-content">${this._formatMessageContent(message.content)}</div>` : '';
4018
+
4019
+ const bubbleHtml = contentHtml ? `<div class="messenger-message-bubble">${contentHtml}</div>` : '';
4017
4020
 
4018
4021
  if (isOwn) {
4019
4022
  return `
4020
4023
  <div class="messenger-message ${messageClass}">
4021
- <div class="messenger-message-bubble">
4022
- <div class="messenger-message-content">${this._formatMessageContent(message.content)}</div>
4023
- </div>
4024
+ ${bubbleHtml}
4025
+ ${attachmentsHtml}
4024
4026
  <div class="messenger-message-time">${timeStr}</div>
4025
4027
  </div>
4026
4028
  `;
@@ -4032,9 +4034,8 @@
4032
4034
  <div class="messenger-message-avatar">${avatarHtml}</div>
4033
4035
  <div class="messenger-message-wrapper">
4034
4036
  <div class="messenger-message-sender">${message.sender?.name || 'Support'}</div>
4035
- <div class="messenger-message-bubble">
4036
- <div class="messenger-message-content">${this._formatMessageContent(message.content)}</div>
4037
- </div>
4037
+ ${bubbleHtml}
4038
+ ${attachmentsHtml}
4038
4039
  <div class="messenger-message-time">${timeStr}</div>
4039
4040
  </div>
4040
4041
  </div>
@@ -4130,6 +4131,49 @@
4130
4131
  }
4131
4132
  }
4132
4133
 
4134
+ _updateSendButtonState() {
4135
+ const input = this.element.querySelector('.messenger-compose-input');
4136
+ const sendBtn = this.element.querySelector('.messenger-compose-send');
4137
+ if (input && sendBtn) {
4138
+ sendBtn.disabled = !input.value.trim() && this._pendingAttachments.length === 0;
4139
+ }
4140
+ }
4141
+
4142
+ _renderAttachmentPreviews() {
4143
+ const container = this.element.querySelector('.messenger-compose-attachments-preview');
4144
+ if (!container) return;
4145
+
4146
+ if (this._pendingAttachments.length === 0) {
4147
+ container.innerHTML = '';
4148
+ container.style.display = 'none';
4149
+ return;
4150
+ }
4151
+
4152
+ container.style.display = 'flex';
4153
+ container.innerHTML = this._pendingAttachments.map((att, i) => {
4154
+ const isImage = att.type.startsWith('image');
4155
+ const thumb = isImage
4156
+ ? `<img class="messenger-attachment-thumb" src="${att.preview}" alt="${this._escapeHtml(att.file.name)}" />`
4157
+ : `<div class="messenger-attachment-thumb messenger-attachment-file-icon"><i class="ph ph-file" style="font-size:20px;"></i></div>`;
4158
+ return `
4159
+ <div class="messenger-attachment-preview" data-index="${i}">
4160
+ ${thumb}
4161
+ <button class="messenger-attachment-remove" data-index="${i}" aria-label="Remove">&times;</button>
4162
+ </div>
4163
+ `;
4164
+ }).join('');
4165
+
4166
+ // Attach remove button events
4167
+ container.querySelectorAll('.messenger-attachment-remove').forEach((btn) => {
4168
+ btn.addEventListener('click', (e) => {
4169
+ const idx = parseInt(e.currentTarget.dataset.index, 10);
4170
+ this._pendingAttachments.splice(idx, 1);
4171
+ this._renderAttachmentPreviews();
4172
+ this._updateSendButtonState();
4173
+ });
4174
+ });
4175
+ }
4176
+
4133
4177
  _attachEvents() {
4134
4178
  this.element
4135
4179
  .querySelector('.messenger-back-btn')
@@ -4143,45 +4187,250 @@
4143
4187
  this.state.setOpen(false);
4144
4188
  });
4145
4189
 
4190
+ // Compose input (not rendered when conversation is closed)
4146
4191
  const input = this.element.querySelector('.messenger-compose-input');
4147
4192
  const sendBtn = this.element.querySelector('.messenger-compose-send');
4148
4193
 
4149
- input.addEventListener('input', () => {
4150
- input.style.height = 'auto';
4151
- input.style.height = Math.min(input.scrollHeight, 120) + 'px';
4194
+ if (input && sendBtn) {
4195
+ input.addEventListener('input', () => {
4196
+ // Auto-resize textarea
4197
+ input.style.height = 'auto';
4198
+ input.style.height = Math.min(input.scrollHeight, 120) + 'px';
4152
4199
 
4153
- sendBtn.disabled = !input.value.trim();
4200
+ // Enable/disable send button
4201
+ this._updateSendButtonState();
4154
4202
 
4155
- if (input.value.trim()) {
4156
- this._startTyping();
4157
- }
4158
- });
4203
+ // Send typing indicator
4204
+ if (input.value.trim()) {
4205
+ this._startTyping();
4206
+ }
4207
+ });
4208
+
4209
+ input.addEventListener('keydown', (e) => {
4210
+ if (e.key === 'Enter' && !e.shiftKey) {
4211
+ e.preventDefault();
4212
+ this._sendMessage();
4213
+ }
4214
+ });
4159
4215
 
4160
- input.addEventListener('keydown', (e) => {
4161
- if (e.key === 'Enter' && !e.shiftKey) {
4162
- e.preventDefault();
4216
+ sendBtn.addEventListener('click', () => {
4163
4217
  this._sendMessage();
4218
+ });
4219
+ }
4220
+
4221
+ // Attach button + file input
4222
+ const attachBtn = this.element.querySelector('.messenger-compose-attach');
4223
+ const fileInput = this.element.querySelector('.messenger-compose-file-input');
4224
+
4225
+ if (attachBtn && fileInput) {
4226
+ attachBtn.addEventListener('click', () => {
4227
+ fileInput.click();
4228
+ });
4229
+
4230
+ fileInput.addEventListener('change', (e) => {
4231
+ const files = e.target.files;
4232
+ if (!files) return;
4233
+ Array.from(files).forEach((file) => {
4234
+ const reader = new FileReader();
4235
+ reader.onload = (ev) => {
4236
+ this._pendingAttachments.push({
4237
+ file,
4238
+ preview: ev.target.result,
4239
+ type: file.type,
4240
+ });
4241
+ this._renderAttachmentPreviews();
4242
+ this._updateSendButtonState();
4243
+ };
4244
+ reader.readAsDataURL(file);
4245
+ });
4246
+ fileInput.value = '';
4247
+ });
4248
+ }
4249
+
4250
+ // Email overlay events
4251
+ const emailInput = this.element.querySelector('.messenger-email-input');
4252
+ const emailSubmit = this.element.querySelector('.messenger-email-submit');
4253
+ const emailSkip = this.element.querySelector('.messenger-email-skip');
4254
+
4255
+ if (emailInput) {
4256
+ emailInput.addEventListener('input', () => {
4257
+ const isValid = this._isValidEmail(emailInput.value.trim());
4258
+ emailSubmit.disabled = !isValid;
4259
+ });
4260
+
4261
+ emailInput.addEventListener('keydown', (e) => {
4262
+ if (e.key === 'Enter' && !emailSubmit.disabled) {
4263
+ e.preventDefault();
4264
+ this._handleEmailSubmit();
4265
+ }
4266
+ });
4267
+ }
4268
+
4269
+ if (emailSubmit) {
4270
+ emailSubmit.addEventListener('click', () => {
4271
+ this._handleEmailSubmit();
4272
+ });
4273
+ }
4274
+
4275
+ if (emailSkip) {
4276
+ emailSkip.addEventListener('click', () => {
4277
+ this._hideEmailOverlay();
4278
+ });
4279
+ }
4280
+
4281
+ // Delegated events for attachment clicks
4282
+ const messagesContainer = this.element.querySelector('.messenger-chat-messages');
4283
+ if (messagesContainer) {
4284
+ messagesContainer.addEventListener('click', (e) => {
4285
+ // File click -> download
4286
+ const fileLink = e.target.closest('.messenger-message-file');
4287
+ if (fileLink) {
4288
+ e.preventDefault();
4289
+ const url = fileLink.dataset.url;
4290
+ const name = fileLink.dataset.name;
4291
+ this._downloadFile(url, name);
4292
+ return;
4293
+ }
4294
+
4295
+ // Image click -> open in new tab
4296
+ const img = e.target.closest('.messenger-message-image');
4297
+ if (img) {
4298
+ const url = img.dataset.url || img.src;
4299
+ window.open(url, '_blank');
4300
+ }
4301
+ });
4302
+ }
4303
+ }
4304
+
4305
+ async _downloadFile(url, name) {
4306
+ try {
4307
+ const response = await fetch(url);
4308
+ const blob = await response.blob();
4309
+ const blobUrl = URL.createObjectURL(blob);
4310
+ const a = document.createElement('a');
4311
+ a.href = blobUrl;
4312
+ a.download = name || 'download';
4313
+ a.style.display = 'none';
4314
+ document.body.appendChild(a);
4315
+ a.click();
4316
+ document.body.removeChild(a);
4317
+ URL.revokeObjectURL(blobUrl);
4318
+ } catch {
4319
+ // Fallback: open in new tab
4320
+ window.open(url, '_blank');
4321
+ }
4322
+ }
4323
+
4324
+ _escapeHtml(text) {
4325
+ if (!text) return '';
4326
+ return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
4327
+ }
4328
+
4329
+ _isValidEmail(email) {
4330
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
4331
+ }
4332
+
4333
+ _showEmailOverlay() {
4334
+ const overlay = this.element.querySelector('.messenger-email-overlay');
4335
+ if (overlay) {
4336
+ overlay.style.display = 'flex';
4337
+ const emailInput = overlay.querySelector('.messenger-email-input');
4338
+ if (emailInput) {
4339
+ setTimeout(() => emailInput.focus(), 100);
4164
4340
  }
4165
- });
4341
+ }
4342
+ }
4166
4343
 
4167
- sendBtn.addEventListener('click', () => {
4168
- this._sendMessage();
4169
- });
4344
+ _startPendingConversation() {
4345
+ if (this._pendingMessage && this.options.onStartConversation) {
4346
+ this.options.onStartConversation(this._pendingMessage, this._pendingAttachmentsForSend || []);
4347
+ this._pendingMessage = null;
4348
+ this._pendingAttachmentsForSend = null;
4349
+ }
4350
+ }
4351
+
4352
+ _hideEmailOverlay() {
4353
+ this._showEmailOverlayFlag = false;
4354
+ const overlay = this.element.querySelector('.messenger-email-overlay');
4355
+ if (overlay) {
4356
+ overlay.style.display = 'none';
4357
+ }
4358
+ }
4359
+
4360
+ async _handleEmailSubmit() {
4361
+ const nameInput = this.element.querySelector('.messenger-email-name');
4362
+ const emailInput = this.element.querySelector('.messenger-email-input');
4363
+ const submitBtn = this.element.querySelector('.messenger-email-submit');
4364
+
4365
+ const name = nameInput?.value.trim() || '';
4366
+ const email = emailInput?.value.trim();
4367
+
4368
+ if (!email || !this._isValidEmail(email)) return;
4369
+
4370
+ submitBtn.disabled = true;
4371
+ submitBtn.textContent = 'Saving...';
4372
+
4373
+ try {
4374
+ if (this.options.onIdentifyContact) {
4375
+ await this.options.onIdentifyContact({ name, email });
4376
+ }
4377
+
4378
+ if (!this.state.userContext) {
4379
+ this.state.userContext = {};
4380
+ }
4381
+ this.state.userContext.name = name;
4382
+ this.state.userContext.email = email;
4383
+
4384
+ this._hideEmailOverlay();
4385
+ this._startPendingConversation();
4386
+ } catch (error) {
4387
+ console.error('[ChatView] Failed to save email:', error);
4388
+ submitBtn.disabled = false;
4389
+ submitBtn.textContent = 'Set my email';
4390
+ }
4170
4391
  }
4171
4392
 
4172
- _sendMessage() {
4393
+ async _sendMessage() {
4394
+ if (this._isConversationClosed) return;
4173
4395
  const input = this.element.querySelector('.messenger-compose-input');
4174
4396
  const content = input.value.trim();
4397
+ const hasAttachments = this._pendingAttachments.length > 0;
4175
4398
 
4176
- if (!content) return;
4399
+ if (!content && !hasAttachments) return;
4177
4400
 
4178
4401
  this._stopTyping();
4179
4402
 
4403
+ // Collect attachments to upload
4404
+ const attachmentsToSend = [...this._pendingAttachments];
4405
+
4180
4406
  const isNewConversation = !this.state.activeConversationId;
4407
+ const needsContactInfo = !this.state.userContext?.email;
4181
4408
 
4182
4409
  if (isNewConversation) {
4183
- if (this.options.onStartConversation) {
4184
- this.options.onStartConversation(content);
4410
+ // Show user's message in chat immediately
4411
+ const localMessage = {
4412
+ id: 'msg_' + Date.now(),
4413
+ content: content,
4414
+ isOwn: true,
4415
+ timestamp: new Date().toISOString(),
4416
+ attachments: attachmentsToSend.map((a) => ({
4417
+ url: a.preview,
4418
+ type: a.type.startsWith('image') ? 'image' : 'file',
4419
+ name: a.file.name,
4420
+ })),
4421
+ };
4422
+ this._appendMessage(localMessage);
4423
+ this._scrollToBottom();
4424
+
4425
+ if (needsContactInfo) {
4426
+ this._pendingMessage = content;
4427
+ this._pendingAttachmentsForSend = attachmentsToSend;
4428
+ this._showEmailOverlayFlag = true;
4429
+ setTimeout(() => this._showEmailOverlay(), 300);
4430
+ } else {
4431
+ if (this.options.onStartConversation) {
4432
+ this.options.onStartConversation(content, attachmentsToSend);
4433
+ }
4185
4434
  }
4186
4435
  } else {
4187
4436
  const message = {
@@ -4189,21 +4438,30 @@
4189
4438
  content: content,
4190
4439
  isOwn: true,
4191
4440
  timestamp: new Date().toISOString(),
4441
+ attachments: attachmentsToSend.map((a) => ({
4442
+ url: a.preview,
4443
+ type: a.type.startsWith('image') ? 'image' : 'file',
4444
+ name: a.file.name,
4445
+ })),
4192
4446
  };
4193
4447
 
4194
4448
  this.state.addMessage(this.state.activeConversationId, message);
4195
4449
 
4196
4450
  if (this.options.onSendMessage) {
4197
- this.options.onSendMessage(this.state.activeConversationId, message);
4451
+ this.options.onSendMessage(this.state.activeConversationId, message, attachmentsToSend);
4198
4452
  }
4199
4453
  }
4200
4454
 
4455
+ // Clear input and attachments
4201
4456
  input.value = '';
4202
4457
  input.style.height = 'auto';
4203
- this.element.querySelector('.messenger-compose-send').disabled = true;
4458
+ this._pendingAttachments = [];
4459
+ this._renderAttachmentPreviews();
4460
+ this._updateSendButtonState();
4204
4461
  }
4205
4462
 
4206
4463
  _startTyping() {
4464
+ if (this._isConversationClosed) return;
4207
4465
  if (!this._isTyping && this.state.activeConversationId) {
4208
4466
  this._isTyping = true;
4209
4467
  if (this.options.onTyping) {
@@ -4284,7 +4542,8 @@
4284
4542
  if (
4285
4543
  type === 'conversationsUpdate' ||
4286
4544
  type === 'conversationAdded' ||
4287
- type === 'conversationRead'
4545
+ type === 'conversationRead' ||
4546
+ type === 'conversationUpdated'
4288
4547
  ) {
4289
4548
  this._updateContent();
4290
4549
  }
@@ -4468,11 +4727,25 @@
4468
4727
  }
4469
4728
 
4470
4729
  _startNewConversation() {
4471
- this.state.setActiveConversation(null);
4472
- this.state.setView('chat');
4730
+ // If there's an open conversation, route to it instead of creating new
4731
+ const openConversation = this.state.conversations.find(
4732
+ (c) => c.status === 'open'
4733
+ );
4473
4734
 
4474
- if (this.options.onStartNewConversation) {
4475
- this.options.onStartNewConversation();
4735
+ if (openConversation) {
4736
+ this.state.setActiveConversation(openConversation.id);
4737
+ this.state.markAsRead(openConversation.id);
4738
+ this.state.setView('chat');
4739
+ if (this.options.onSelectConversation) {
4740
+ this.options.onSelectConversation(openConversation.id);
4741
+ }
4742
+ } else {
4743
+ this.state.setActiveConversation(null);
4744
+ if (this.options.onStartNewConversation) {
4745
+ this.options.onStartNewConversation();
4746
+ } else {
4747
+ this.state.setView('chat');
4748
+ }
4476
4749
  }
4477
4750
  }
4478
4751
 
@@ -4731,12 +5004,7 @@
4731
5004
  </div>
4732
5005
 
4733
5006
  <div class="messenger-home-body">
4734
- <button class="messenger-home-message-btn">
4735
- <span>Send us a message</span>
4736
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#000000" viewBox="0 0 256 256">
4737
- <path d="M221.66,133.66l-72,72a8,8,0,0,1-11.32-11.32L196.69,136H40a8,8,0,0,1,0-16H196.69L138.34,61.66a8,8,0,0,1,11.32-11.32l72,72A8,8,0,0,1,221.66,133.66Z"></path>
4738
- </svg>
4739
- </button>
5007
+ ${this._renderMessageButton()}
4740
5008
 
4741
5009
  ${this._renderFeaturedCard()}
4742
5010
 
@@ -4776,156 +5044,698 @@
4776
5044
  return colors[index % colors.length];
4777
5045
  }
4778
5046
 
4779
- _renderAvailabilityStatus() {
4780
- const isOnline = this.state.agentsOnline;
4781
- const responseTime =
4782
- this.state.responseTime || 'We typically reply within a few minutes';
5047
+ _renderAvailabilityStatus() {
5048
+ const isOnline = this.state.agentsOnline;
5049
+ const responseTime =
5050
+ this.state.responseTime || 'We typically reply within a few minutes';
5051
+
5052
+ if (isOnline) {
5053
+ return `
5054
+ <div class="messenger-home-availability">
5055
+ <span class="messenger-availability-dot messenger-availability-online"></span>
5056
+ <span class="messenger-availability-text">We're online now</span>
5057
+ </div>
5058
+ `;
5059
+ }
5060
+
5061
+ return `
5062
+ <div class="messenger-home-availability">
5063
+ <span class="messenger-availability-dot messenger-availability-away"></span>
5064
+ <span class="messenger-availability-text">${responseTime}</span>
5065
+ </div>
5066
+ `;
5067
+ }
5068
+
5069
+ _renderMessageButton() {
5070
+ const openConversation = this.state.conversations.find(
5071
+ (c) => c.status === 'open'
5072
+ );
5073
+
5074
+ if (openConversation) {
5075
+ const preview = openConversation.lastMessage
5076
+ ? (openConversation.lastMessage.length > 40
5077
+ ? openConversation.lastMessage.substring(0, 40) + '...'
5078
+ : openConversation.lastMessage)
5079
+ : 'Continue your conversation';
5080
+
5081
+ return `
5082
+ <button class="messenger-home-message-btn messenger-home-continue-btn" data-conversation-id="${openConversation.id}">
5083
+ <div class="messenger-home-continue-info">
5084
+ <span class="messenger-home-continue-label">Continue conversation</span>
5085
+ <span class="messenger-home-continue-preview">${preview}</span>
5086
+ </div>
5087
+ <i class="ph ph-arrow-right" style="font-size: 16px;"></i>
5088
+ </button>
5089
+ `;
5090
+ }
5091
+
5092
+ return `
5093
+ <button class="messenger-home-message-btn">
5094
+ <span>Send us a message</span>
5095
+ <i class="ph ph-arrow-right" style="font-size: 16px;"></i>
5096
+ </button>
5097
+ `;
5098
+ }
5099
+
5100
+ _renderFeaturedCard() {
5101
+ if (!this.options.featuredContent) {
5102
+ return '';
5103
+ }
5104
+
5105
+ const { title, description, imageUrl, action } =
5106
+ this.options.featuredContent;
5107
+
5108
+ return `
5109
+ <div class="messenger-home-featured">
5110
+ ${imageUrl ? `<img src="${imageUrl}" alt="${title}" class="messenger-home-featured-image" onerror="this.style.display='none';" />` : ''}
5111
+ <div class="messenger-home-featured-content">
5112
+ <h3>${title}</h3>
5113
+ <p>${description}</p>
5114
+ </div>
5115
+ ${action ? `<button class="messenger-home-featured-btn" data-action="${action.type}" data-value="${action.value}">${action.label}</button>` : ''}
5116
+ </div>
5117
+ `;
5118
+ }
5119
+
5120
+ _renderRecentChangelog() {
5121
+ const changelogItems = this.state.homeChangelogItems;
5122
+ if (changelogItems.length === 0) {
5123
+ return '';
5124
+ }
5125
+
5126
+ const changelogHtml = changelogItems
5127
+ .map(
5128
+ (item) => `
5129
+ <div class="messenger-home-changelog-card" data-changelog-id="${item.id}">
5130
+ ${
5131
+ item.coverImage
5132
+ ? `
5133
+ <div class="messenger-home-changelog-cover">
5134
+ <img src="${item.coverImage}" alt="${item.title}" onerror="this.style.display='none';" />
5135
+ ${item.coverText ? `<span class="messenger-home-changelog-cover-text">${item.coverText}</span>` : ''}
5136
+ </div>
5137
+ `
5138
+ : ''
5139
+ }
5140
+ <div class="messenger-home-changelog-card-content">
5141
+ <h4 class="messenger-home-changelog-card-title">${item.title}</h4>
5142
+ <p class="messenger-home-changelog-card-desc">${item.description || ''}</p>
5143
+ </div>
5144
+ </div>
5145
+ `
5146
+ )
5147
+ .join('');
5148
+
5149
+ return `
5150
+ <div class="messenger-home-changelog-section">
5151
+ ${changelogHtml}
5152
+ </div>
5153
+ `;
5154
+ }
5155
+
5156
+ _formatDate(dateString) {
5157
+ if (!dateString) return '';
5158
+ const date = new Date(dateString);
5159
+ const now = new Date();
5160
+ const diffDays = Math.floor((now - date) / (1000 * 60 * 60 * 24));
5161
+
5162
+ if (diffDays === 0) return 'Today';
5163
+ if (diffDays === 1) return 'Yesterday';
5164
+ if (diffDays < 7) return `${diffDays}d ago`;
5165
+ return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
5166
+ }
5167
+
5168
+ _attachEvents() {
5169
+ this.element
5170
+ .querySelector('.messenger-close-btn')
5171
+ .addEventListener('click', () => {
5172
+ this.state.setOpen(false);
5173
+ });
5174
+
5175
+ // Send message / continue conversation button
5176
+ const msgBtn = this.element.querySelector('.messenger-home-message-btn');
5177
+ if (msgBtn) {
5178
+ msgBtn.addEventListener('click', () => {
5179
+ const convId = msgBtn.dataset.conversationId;
5180
+ if (convId) {
5181
+ // Continue existing open conversation
5182
+ this.state.setActiveConversation(convId);
5183
+ this.state.setView('chat');
5184
+ if (this.options.onSelectConversation) {
5185
+ this.options.onSelectConversation(convId);
5186
+ }
5187
+ } else {
5188
+ // No open conversation — start new
5189
+ this.state.setActiveConversation(null);
5190
+ this.state.setView('chat');
5191
+ }
5192
+ });
5193
+ }
5194
+
5195
+ this.element
5196
+ .querySelectorAll('.messenger-home-changelog-item')
5197
+ .forEach((item) => {
5198
+ item.addEventListener('click', () => {
5199
+ this.state.setView('changelog');
5200
+ });
5201
+ });
5202
+
5203
+ const seeAllBtn = this.element.querySelector(
5204
+ '.messenger-home-changelog-all'
5205
+ );
5206
+ if (seeAllBtn) {
5207
+ seeAllBtn.addEventListener('click', () => {
5208
+ this.state.setView('changelog');
5209
+ });
5210
+ }
5211
+
5212
+ const featuredBtn = this.element.querySelector(
5213
+ '.messenger-home-featured-btn'
5214
+ );
5215
+ if (featuredBtn) {
5216
+ featuredBtn.addEventListener('click', () => {
5217
+ const action = featuredBtn.dataset.action;
5218
+ const value = featuredBtn.dataset.value;
5219
+ if (action === 'url') {
5220
+ window.open(value, '_blank');
5221
+ } else if (action === 'view') {
5222
+ this.state.setView(value);
5223
+ }
5224
+ });
5225
+ }
5226
+ }
5227
+
5228
+ destroy() {
5229
+ if (this._unsubscribe) {
5230
+ this._unsubscribe();
5231
+ }
5232
+ if (this.element && this.element.parentNode) {
5233
+ this.element.parentNode.removeChild(this.element);
5234
+ }
5235
+ }
5236
+ }
5237
+
5238
+ /**
5239
+ * PreChatFormView - Collects user info after the first message
5240
+ */
5241
+ class PreChatFormView {
5242
+ constructor(state, options = {}) {
5243
+ this.state = state;
5244
+ this.options = options;
5245
+ this.element = null;
5246
+ this._isSubmitting = false;
5247
+ }
5248
+
5249
+ render() {
5250
+ this.element = document.createElement('div');
5251
+ this.element.className = 'messenger-view messenger-prechat-view';
5252
+
5253
+ this._updateContent();
5254
+
5255
+ return this.element;
5256
+ }
5257
+
5258
+ _updateContent() {
5259
+ // Pre-fill from userContext if available
5260
+ const existingName = this.state.userContext?.name || '';
5261
+ const existingEmail = this.state.userContext?.email || '';
5262
+
5263
+ this.element.innerHTML = `
5264
+ <div class="messenger-prechat-overlay">
5265
+ <div class="messenger-prechat-card">
5266
+ <h4>Get notified when we reply</h4>
5267
+ <form class="messenger-prechat-form" novalidate>
5268
+ <div class="messenger-prechat-fields">
5269
+ <input
5270
+ type="text"
5271
+ id="messenger-prechat-name"
5272
+ name="name"
5273
+ placeholder="Name (optional)"
5274
+ value="${this._escapeHtml(existingName)}"
5275
+ autocomplete="name"
5276
+ class="messenger-prechat-input"
5277
+ />
5278
+ <input
5279
+ type="email"
5280
+ id="messenger-prechat-email"
5281
+ name="email"
5282
+ placeholder="Email address"
5283
+ value="${this._escapeHtml(existingEmail)}"
5284
+ required
5285
+ autocomplete="email"
5286
+ class="messenger-prechat-input"
5287
+ />
5288
+ </div>
5289
+ <span class="messenger-prechat-error" id="messenger-email-error"></span>
5290
+ <div class="messenger-prechat-actions">
5291
+ <button type="button" class="messenger-prechat-skip">Skip</button>
5292
+ <button type="submit" class="messenger-prechat-submit" disabled>
5293
+ <span class="messenger-prechat-submit-text">Continue</span>
5294
+ <span class="messenger-prechat-submit-loading" style="display: none;">
5295
+ <i class="ph ph-spinner" style="font-size: 16px;"></i>
5296
+ </span>
5297
+ </button>
5298
+ </div>
5299
+ </form>
5300
+ </div>
5301
+ </div>
5302
+ `;
5303
+
5304
+ this._attachEvents();
5305
+ }
5306
+
5307
+ _renderTeamAvatars() {
5308
+ const avatars = this.state.teamAvatars;
5309
+ if (!avatars || avatars.length === 0) {
5310
+ return `
5311
+ <div class="messenger-avatar-stack">
5312
+ <div class="messenger-avatar" style="background: #5856d6;">S</div>
5313
+ <div class="messenger-avatar" style="background: #007aff;">T</div>
5314
+ </div>
5315
+ `;
5316
+ }
5317
+
5318
+ const colors = ['#5856d6', '#007aff', '#34c759', '#ff9500', '#ff3b30'];
5319
+ const avatarItems = avatars
5320
+ .slice(0, 3)
5321
+ .map((avatar, i) => {
5322
+ if (typeof avatar === 'string' && avatar.startsWith('http')) {
5323
+ return `<img class="messenger-avatar" src="${avatar}" alt="Team member" style="z-index: ${3 - i};" />`;
5324
+ }
5325
+ return `<div class="messenger-avatar" style="background: ${colors[i % colors.length]}; z-index: ${3 - i};">${avatar.charAt(0).toUpperCase()}</div>`;
5326
+ })
5327
+ .join('');
5328
+
5329
+ return `<div class="messenger-avatar-stack">${avatarItems}</div>`;
5330
+ }
5331
+
5332
+ _escapeHtml(text) {
5333
+ if (!text) return '';
5334
+ return text
5335
+ .replace(/&/g, '&amp;')
5336
+ .replace(/</g, '&lt;')
5337
+ .replace(/>/g, '&gt;')
5338
+ .replace(/"/g, '&quot;');
5339
+ }
5340
+
5341
+ _attachEvents() {
5342
+ // Form validation
5343
+ const form = this.element.querySelector('.messenger-prechat-form');
5344
+ const emailInput = this.element.querySelector('#messenger-prechat-email');
5345
+ const submitBtn = this.element.querySelector('.messenger-prechat-submit');
5346
+ const skipBtn = this.element.querySelector('.messenger-prechat-skip');
5347
+
5348
+ const validateForm = () => {
5349
+ const email = emailInput.value.trim();
5350
+ const isEmailValid = this._isValidEmail(email);
5351
+ submitBtn.disabled = !isEmailValid;
5352
+ return isEmailValid;
5353
+ };
5354
+
5355
+ emailInput.addEventListener('input', () => {
5356
+ this._clearError('messenger-email-error');
5357
+ validateForm();
5358
+ });
5359
+
5360
+ emailInput.addEventListener('blur', () => {
5361
+ const email = emailInput.value.trim();
5362
+ if (email && !this._isValidEmail(email)) {
5363
+ this._showError('messenger-email-error', 'Please enter a valid email');
5364
+ }
5365
+ });
5366
+
5367
+ // Skip button - go back to chat without collecting info
5368
+ skipBtn.addEventListener('click', () => {
5369
+ this.state.setView('chat');
5370
+ });
5371
+
5372
+ // Form submission
5373
+ form.addEventListener('submit', async (e) => {
5374
+ e.preventDefault();
5375
+ if (this._isSubmitting) return;
5376
+ if (!validateForm()) {
5377
+ this._showError('messenger-email-error', 'Please enter a valid email');
5378
+ return;
5379
+ }
5380
+ await this._handleSubmit();
5381
+ });
5382
+
5383
+ // Set initial button state
5384
+ validateForm();
5385
+
5386
+ // Focus email input
5387
+ setTimeout(() => emailInput.focus(), 100);
5388
+ }
5389
+
5390
+ _isValidEmail(email) {
5391
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
5392
+ return emailRegex.test(email);
5393
+ }
5394
+
5395
+ _showError(elementId, message) {
5396
+ const errorEl = this.element.querySelector(`#${elementId}`);
5397
+ if (errorEl) {
5398
+ errorEl.textContent = message;
5399
+ errorEl.style.display = 'block';
5400
+ }
5401
+ }
5402
+
5403
+ _clearError(elementId) {
5404
+ const errorEl = this.element.querySelector(`#${elementId}`);
5405
+ if (errorEl) {
5406
+ errorEl.textContent = '';
5407
+ errorEl.style.display = 'none';
5408
+ }
5409
+ }
5410
+
5411
+ async _handleSubmit() {
5412
+ const nameInput = this.element.querySelector('#messenger-prechat-name');
5413
+ const emailInput = this.element.querySelector('#messenger-prechat-email');
5414
+ const submitBtn = this.element.querySelector('.messenger-prechat-submit');
5415
+ const submitText = submitBtn.querySelector('.messenger-prechat-submit-text');
5416
+ const submitLoading = submitBtn.querySelector('.messenger-prechat-submit-loading');
5417
+
5418
+ const name = nameInput.value.trim();
5419
+ const email = emailInput.value.trim();
5420
+
5421
+ // Show loading state
5422
+ this._isSubmitting = true;
5423
+ submitBtn.disabled = true;
5424
+ submitText.style.display = 'none';
5425
+ submitLoading.style.display = 'inline-flex';
5426
+
5427
+ try {
5428
+ // First, identify the contact with collected info
5429
+ if (this.options.onIdentifyContact) {
5430
+ await this.options.onIdentifyContact({ name, email });
5431
+ }
5432
+
5433
+ // Update state with user info
5434
+ if (!this.state.userContext) {
5435
+ this.state.userContext = {};
5436
+ }
5437
+ this.state.userContext.name = name;
5438
+ this.state.userContext.email = email;
5439
+
5440
+ this._isSubmitting = false;
5441
+
5442
+ // Go to chat after collecting contact info
5443
+ this.state.setView('chat');
5444
+ } catch (error) {
5445
+ console.error('[PreChatFormView] Error submitting form:', error);
5446
+ this._showError('messenger-email-error', 'Something went wrong. Please try again.');
5447
+
5448
+ // Reset button state
5449
+ this._isSubmitting = false;
5450
+ submitBtn.disabled = false;
5451
+ submitText.style.display = 'inline';
5452
+ submitLoading.style.display = 'none';
5453
+ }
5454
+ }
5455
+
5456
+ destroy() {
5457
+ if (this.element && this.element.parentNode) {
5458
+ this.element.parentNode.removeChild(this.element);
5459
+ }
5460
+ }
5461
+ }
5462
+
5463
+ /**
5464
+ * WebSocketService - Real-time communication for messenger widget
5465
+ */
5466
+
5467
+ class WebSocketService {
5468
+ constructor(config = {}) {
5469
+ this.baseURL = config.baseURL || '';
5470
+ this.workspace = config.workspace || '';
5471
+ this.sessionToken = config.sessionToken || null;
5472
+ this.mock = config.mock || false;
5473
+
5474
+ this.ws = null;
5475
+ this.reconnectAttempts = 0;
5476
+ this.maxReconnectAttempts = 5;
5477
+ this.reconnectDelay = 1000;
5478
+ this.pingInterval = null;
5479
+ this.isConnected = false;
5480
+
5481
+ // Event listeners
5482
+ this._listeners = new Map();
5483
+
5484
+ // Bind methods
5485
+ this._onOpen = this._onOpen.bind(this);
5486
+ this._onMessage = this._onMessage.bind(this);
5487
+ this._onClose = this._onClose.bind(this);
5488
+ this._onError = this._onError.bind(this);
5489
+ }
5490
+
5491
+ /**
5492
+ * Connect to WebSocket server
5493
+ */
5494
+ connect(sessionToken = null) {
5495
+ if (sessionToken) {
5496
+ this.sessionToken = sessionToken;
5497
+ }
5498
+
5499
+ if (!this.sessionToken) {
5500
+ console.warn('[WebSocket] No session token provided');
5501
+ return;
5502
+ }
5503
+
5504
+ // Mock mode - simulate connection
5505
+ if (this.mock) {
5506
+ this.isConnected = true;
5507
+ this._emit('connected', {});
5508
+ this._startMockResponses();
5509
+ return;
5510
+ }
5511
+
5512
+ // Build WebSocket URL
5513
+ const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
5514
+ let wsURL = this.baseURL.replace(/^https?:/, wsProtocol);
5515
+ wsURL = wsURL.replace('/api/v1', '');
5516
+ wsURL = `${wsURL}/api/v1/widget/messenger/ws?token=${encodeURIComponent(this.sessionToken)}`;
5517
+
5518
+ try {
5519
+ this.ws = new WebSocket(wsURL);
5520
+ this.ws.onopen = this._onOpen;
5521
+ this.ws.onmessage = this._onMessage;
5522
+ this.ws.onclose = this._onClose;
5523
+ this.ws.onerror = this._onError;
5524
+ } catch (error) {
5525
+ console.error('[WebSocket] Connection error:', error);
5526
+ this._scheduleReconnect();
5527
+ }
5528
+ }
5529
+
5530
+ /**
5531
+ * Disconnect from WebSocket server
5532
+ */
5533
+ disconnect() {
5534
+ this.isConnected = false;
5535
+ this.reconnectAttempts = this.maxReconnectAttempts; // Prevent reconnection
5536
+
5537
+ if (this.pingInterval) {
5538
+ clearInterval(this.pingInterval);
5539
+ this.pingInterval = null;
5540
+ }
5541
+
5542
+ if (this.ws) {
5543
+ this.ws.close();
5544
+ this.ws = null;
5545
+ }
5546
+
5547
+ if (this._mockInterval) {
5548
+ clearInterval(this._mockInterval);
5549
+ this._mockInterval = null;
5550
+ }
5551
+ }
5552
+
5553
+ /**
5554
+ * Subscribe to events
5555
+ * @param {string} event - Event name
5556
+ * @param {Function} callback - Event handler
5557
+ * @returns {Function} Unsubscribe function
5558
+ */
5559
+ on(event, callback) {
5560
+ if (!this._listeners.has(event)) {
5561
+ this._listeners.set(event, new Set());
5562
+ }
5563
+ this._listeners.get(event).add(callback);
5564
+ return () => this._listeners.get(event).delete(callback);
5565
+ }
5566
+
5567
+ /**
5568
+ * Remove event listener
5569
+ */
5570
+ off(event, callback) {
5571
+ if (this._listeners.has(event)) {
5572
+ this._listeners.get(event).delete(callback);
5573
+ }
5574
+ }
5575
+
5576
+ /**
5577
+ * Send message through WebSocket
5578
+ */
5579
+ send(type, payload = {}) {
5580
+ if (!this.isConnected) {
5581
+ console.warn('[WebSocket] Not connected, cannot send message');
5582
+ return;
5583
+ }
5584
+
5585
+ if (this.mock) {
5586
+ // Mock mode - just log
5587
+ console.log('[WebSocket Mock] Sending:', type, payload);
5588
+ return;
5589
+ }
5590
+
5591
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
5592
+ this.ws.send(JSON.stringify({ type, payload }));
5593
+ }
5594
+ }
5595
+
5596
+ // Private methods
5597
+
5598
+ _onOpen() {
5599
+ console.log('[WebSocket] Connected');
5600
+ this.isConnected = true;
5601
+ this.reconnectAttempts = 0;
5602
+ this._emit('connected', {});
5603
+
5604
+ // Start ping interval to keep connection alive
5605
+ this.pingInterval = setInterval(() => {
5606
+ this.send('ping', {});
5607
+ }, 30000);
5608
+ }
5609
+
5610
+ _onMessage(event) {
5611
+ try {
5612
+ const data = JSON.parse(event.data);
5613
+ const { type, payload } = data;
5614
+
5615
+ // Handle different event types
5616
+ switch (type) {
5617
+ case 'message:new':
5618
+ this._emit('message', payload);
5619
+ break;
5620
+ case 'typing:started':
5621
+ this._emit('typing_started', payload);
5622
+ break;
5623
+ case 'typing:stopped':
5624
+ this._emit('typing_stopped', payload);
5625
+ break;
5626
+ case 'conversation:updated':
5627
+ this._emit('conversation_updated', payload);
5628
+ break;
5629
+ case 'conversation:closed':
5630
+ this._emit('conversation_closed', payload);
5631
+ break;
5632
+ case 'availability:changed':
5633
+ this._emit('availability_changed', payload);
5634
+ break;
5635
+ case 'pong':
5636
+ // Ping response, ignore
5637
+ break;
5638
+ default:
5639
+ console.log('[WebSocket] Unknown event:', type, payload);
5640
+ }
5641
+ } catch (error) {
5642
+ console.error('[WebSocket] Failed to parse message:', error);
5643
+ }
5644
+ }
5645
+
5646
+ _onClose(event) {
5647
+ console.log('[WebSocket] Disconnected:', event.code, event.reason);
5648
+ this.isConnected = false;
4783
5649
 
4784
- if (isOnline) {
4785
- return `
4786
- <div class="messenger-home-availability">
4787
- <span class="messenger-availability-dot messenger-availability-online"></span>
4788
- <span class="messenger-availability-text">We're online now</span>
4789
- </div>
4790
- `;
5650
+ if (this.pingInterval) {
5651
+ clearInterval(this.pingInterval);
5652
+ this.pingInterval = null;
4791
5653
  }
4792
5654
 
4793
- return `
4794
- <div class="messenger-home-availability">
4795
- <span class="messenger-availability-dot messenger-availability-away"></span>
4796
- <span class="messenger-availability-text">${responseTime}</span>
4797
- </div>
4798
- `;
5655
+ this._emit('disconnected', { code: event.code, reason: event.reason });
5656
+ this._scheduleReconnect();
4799
5657
  }
4800
5658
 
4801
- _renderFeaturedCard() {
4802
- if (!this.options.featuredContent) {
4803
- return '';
4804
- }
4805
-
4806
- const { title, description, imageUrl, action } =
4807
- this.options.featuredContent;
4808
-
4809
- return `
4810
- <div class="messenger-home-featured">
4811
- ${imageUrl ? `<img src="${imageUrl}" alt="${title}" class="messenger-home-featured-image" onerror="this.style.display='none';" />` : ''}
4812
- <div class="messenger-home-featured-content">
4813
- <h3>${title}</h3>
4814
- <p>${description}</p>
4815
- </div>
4816
- ${action ? `<button class="messenger-home-featured-btn" data-action="${action.type}" data-value="${action.value}">${action.label}</button>` : ''}
4817
- </div>
4818
- `;
5659
+ _onError(error) {
5660
+ console.error('[WebSocket] Error:', error);
5661
+ this._emit('error', { error });
4819
5662
  }
4820
5663
 
4821
- _renderRecentChangelog() {
4822
- const changelogItems = this.state.homeChangelogItems;
4823
- if (changelogItems.length === 0) {
4824
- return '';
5664
+ _scheduleReconnect() {
5665
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) {
5666
+ console.log('[WebSocket] Max reconnect attempts reached');
5667
+ this._emit('reconnect_failed', {});
5668
+ return;
4825
5669
  }
4826
5670
 
4827
- const changelogHtml = changelogItems
4828
- .map(
4829
- (item) => `
4830
- <div class="messenger-home-changelog-card" data-changelog-id="${item.id}">
4831
- ${
4832
- item.coverImage
4833
- ? `
4834
- <div class="messenger-home-changelog-cover">
4835
- <img src="${item.coverImage}" alt="${item.title}" onerror="this.style.display='none';" />
4836
- ${item.coverText ? `<span class="messenger-home-changelog-cover-text">${item.coverText}</span>` : ''}
4837
- </div>
4838
- `
4839
- : ''
4840
- }
4841
- <div class="messenger-home-changelog-card-content">
4842
- <h4 class="messenger-home-changelog-card-title">${item.title}</h4>
4843
- <p class="messenger-home-changelog-card-desc">${item.description || ''}</p>
4844
- </div>
4845
- </div>
4846
- `
4847
- )
4848
- .join('');
4849
-
4850
- return `
4851
- <div class="messenger-home-changelog-section">
4852
- ${changelogHtml}
4853
- </div>
4854
- `;
4855
- }
4856
-
4857
- _formatDate(dateString) {
4858
- if (!dateString) return '';
4859
- const date = new Date(dateString);
4860
- const now = new Date();
4861
- const diffDays = Math.floor((now - date) / (1000 * 60 * 60 * 24));
5671
+ this.reconnectAttempts++;
5672
+ const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
5673
+ console.log(
5674
+ `[WebSocket] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`
5675
+ );
4862
5676
 
4863
- if (diffDays === 0) return 'Today';
4864
- if (diffDays === 1) return 'Yesterday';
4865
- if (diffDays < 7) return `${diffDays}d ago`;
4866
- return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
5677
+ setTimeout(() => {
5678
+ this.connect();
5679
+ }, delay);
4867
5680
  }
4868
5681
 
4869
- _attachEvents() {
4870
- this.element
4871
- .querySelector('.messenger-close-btn')
4872
- .addEventListener('click', () => {
4873
- this.state.setOpen(false);
4874
- });
4875
-
4876
- this.element
4877
- .querySelector('.messenger-home-message-btn')
4878
- .addEventListener('click', () => {
4879
- this.state.setView('messages');
5682
+ _emit(event, data) {
5683
+ if (this._listeners.has(event)) {
5684
+ this._listeners.get(event).forEach((callback) => {
5685
+ try {
5686
+ callback(data);
5687
+ } catch (error) {
5688
+ console.error(`[WebSocket] Error in ${event} handler:`, error);
5689
+ }
4880
5690
  });
5691
+ }
5692
+ }
4881
5693
 
4882
- this.element
4883
- .querySelectorAll('.messenger-home-changelog-item')
4884
- .forEach((item) => {
4885
- item.addEventListener('click', () => {
4886
- this.state.setView('changelog');
5694
+ // Mock support for development
5695
+ _startMockResponses() {
5696
+ // Simulate agent typing and responses
5697
+ this._mockInterval = setInterval(() => {
5698
+ // Randomly emit typing or message events for demo
5699
+ const random = Math.random();
5700
+ if (random < 0.1) {
5701
+ this._emit('typing_started', {
5702
+ conversation_id: 'conv_1',
5703
+ user_id: 'agent_1',
5704
+ user_name: 'Sarah',
5705
+ is_agent: true,
4887
5706
  });
4888
- });
4889
-
4890
- const seeAllBtn = this.element.querySelector(
4891
- '.messenger-home-changelog-all'
4892
- );
4893
- if (seeAllBtn) {
4894
- seeAllBtn.addEventListener('click', () => {
4895
- this.state.setView('changelog');
4896
- });
4897
- }
4898
5707
 
4899
- const featuredBtn = this.element.querySelector(
4900
- '.messenger-home-featured-btn'
4901
- );
4902
- if (featuredBtn) {
4903
- featuredBtn.addEventListener('click', () => {
4904
- const action = featuredBtn.dataset.action;
4905
- const value = featuredBtn.dataset.value;
4906
- if (action === 'url') {
4907
- window.open(value, '_blank');
4908
- } else if (action === 'view') {
4909
- this.state.setView(value);
4910
- }
4911
- });
4912
- }
5708
+ // Stop typing after 2 seconds
5709
+ setTimeout(() => {
5710
+ this._emit('typing_stopped', {
5711
+ conversation_id: 'conv_1',
5712
+ user_id: 'agent_1',
5713
+ });
5714
+ }, 2000);
5715
+ }
5716
+ }, 10000);
4913
5717
  }
4914
5718
 
4915
- destroy() {
4916
- if (this._unsubscribe) {
4917
- this._unsubscribe();
4918
- }
4919
- if (this.element && this.element.parentNode) {
4920
- this.element.parentNode.removeChild(this.element);
5719
+ /**
5720
+ * Simulate receiving a message (for mock mode)
5721
+ */
5722
+ simulateMessage(conversationId, message) {
5723
+ if (this.mock) {
5724
+ this._emit('message', {
5725
+ conversation_id: conversationId,
5726
+ message: {
5727
+ id: 'msg_' + Date.now(),
5728
+ content: message.content,
5729
+ sender_type: message.sender_type || 'agent',
5730
+ sender_name: message.sender_name || 'Support',
5731
+ sender_avatar: message.sender_avatar || null,
5732
+ created_at: new Date().toISOString(),
5733
+ },
5734
+ });
4921
5735
  }
4922
5736
  }
4923
5737
  }
4924
5738
 
4925
- /**
4926
- * MessengerWidget - Full-featured Messenger/Chat widget
4927
- */
4928
-
4929
5739
  class MessengerWidget extends BaseWidget {
4930
5740
  constructor(options) {
4931
5741
  super({ ...options, type: 'messenger' });
@@ -4941,13 +5751,11 @@
4941
5751
  logoUrl: options.logoUrl || 'https://product7.io/p7logo.svg',
4942
5752
  featuredContent: options.featuredContent || null,
4943
5753
  primaryColor: options.primaryColor || '#1c1c1e',
4944
- // Callbacks
4945
5754
  onSendMessage: options.onSendMessage || null,
4946
5755
  onArticleClick: options.onArticleClick || null,
4947
5756
  onChangelogClick: options.onChangelogClick || null,
4948
5757
  };
4949
5758
 
4950
- // Create state
4951
5759
  this.messengerState = new MessengerState({
4952
5760
  teamName: this.messengerOptions.teamName,
4953
5761
  teamAvatars: this.messengerOptions.teamAvatars,
@@ -4962,51 +5770,46 @@
4962
5770
  this.wsService = null;
4963
5771
  this._wsUnsubscribers = [];
4964
5772
 
4965
- // Bind methods
4966
5773
  this._handleOpenChange = this._handleOpenChange.bind(this);
4967
5774
  this._handleWebSocketMessage = this._handleWebSocketMessage.bind(this);
4968
5775
  this._handleTypingStarted = this._handleTypingStarted.bind(this);
4969
5776
  this._handleTypingStopped = this._handleTypingStopped.bind(this);
5777
+ this._handleConversationClosed = this._handleConversationClosed.bind(this);
4970
5778
  }
4971
5779
 
4972
5780
  _render() {
4973
- // Create container
4974
5781
  const container = document.createElement('div');
4975
5782
  container.className = `messenger-widget theme-${this.messengerOptions.theme}`;
4976
5783
  container.style.zIndex = '999999';
4977
5784
 
4978
- // Create launcher
4979
5785
  this.launcher = new MessengerLauncher(this.messengerState, {
4980
5786
  position: this.messengerOptions.position,
4981
5787
  primaryColor: this.messengerOptions.primaryColor,
4982
5788
  });
4983
5789
  container.appendChild(this.launcher.render());
4984
5790
 
4985
- // Create panel with all callbacks
4986
5791
  this.panel = new MessengerPanel(this.messengerState, {
4987
5792
  position: this.messengerOptions.position,
4988
5793
  theme: this.messengerOptions.theme,
4989
5794
  primaryColor: this.messengerOptions.primaryColor,
4990
5795
  logoUrl: this.messengerOptions.logoUrl,
4991
5796
  featuredContent: this.messengerOptions.featuredContent,
4992
- // Chat callbacks
4993
5797
  onSendMessage:
4994
5798
  this.messengerOptions.onSendMessage ||
4995
5799
  this._handleSendMessage.bind(this),
4996
5800
  onStartConversation: this._handleStartConversation.bind(this),
4997
5801
  onTyping: this.sendTypingIndicator.bind(this),
4998
- // Conversation list callbacks
4999
5802
  onSelectConversation: this._handleSelectConversation.bind(this),
5000
5803
  onStartNewConversation: this._handleNewConversationClick.bind(this),
5001
- // Article/changelog callbacks
5804
+ onIdentifyContact: this._handleIdentifyContact.bind(this),
5002
5805
  onArticleClick: this.messengerOptions.onArticleClick,
5003
5806
  onChangelogClick: this.messengerOptions.onChangelogClick,
5004
5807
  });
5005
5808
 
5006
- // Register views
5007
5809
  this.panel.registerView('home', HomeView);
5008
5810
  this.panel.registerView('messages', ConversationsView);
5009
5811
  this.panel.registerView('chat', ChatView);
5812
+ this.panel.registerView('prechat', PreChatFormView);
5010
5813
  this.panel.registerView('help', HelpView);
5011
5814
  this.panel.registerView('changelog', ChangelogView);
5012
5815
 
@@ -5017,11 +5820,16 @@
5017
5820
  }
5018
5821
 
5019
5822
  _attachEvents() {
5020
- // Subscribe to state changes
5021
5823
  this._stateUnsubscribe = this.messengerState.subscribe((type, data) => {
5022
5824
  if (type === 'openChange') {
5023
5825
  this._handleOpenChange(data.isOpen);
5024
5826
  }
5827
+ if (type === 'conversationChange') {
5828
+ this._handleActiveConversationChange(
5829
+ data.conversationId,
5830
+ data.previousConversationId
5831
+ );
5832
+ }
5025
5833
  });
5026
5834
  }
5027
5835
 
@@ -5036,38 +5844,155 @@
5036
5844
  }
5037
5845
  }
5038
5846
 
5039
- /**
5040
- * Handle starting a new conversation
5041
- */
5042
- async _handleStartConversation(messageContent) {
5847
+ _handleActiveConversationChange(conversationId, previousConversationId) {
5848
+ if (previousConversationId && this.wsService) {
5849
+ this.wsService.send('conversation:unsubscribe', {
5850
+ conversation_id: previousConversationId,
5851
+ });
5852
+ }
5853
+ if (conversationId && this.wsService) {
5854
+ this.wsService.send('conversation:subscribe', {
5855
+ conversation_id: conversationId,
5856
+ });
5857
+ }
5858
+ }
5859
+
5860
+ async _handleStartConversation(messageContent, pendingAttachments) {
5043
5861
  try {
5044
- await this.startNewConversation(messageContent);
5862
+ const userContext = this.messengerState.userContext;
5863
+ const isIdentified = userContext?.email;
5864
+
5865
+ if (!isIdentified) {
5866
+ this.messengerState.pendingMessage = {
5867
+ content: messageContent,
5868
+ attachments: pendingAttachments,
5869
+ };
5870
+ this.messengerState.setView('prechat');
5871
+ return null;
5872
+ }
5873
+
5874
+ const openConversation = this.messengerState.conversations.find(
5875
+ (c) => c.status === 'open'
5876
+ );
5877
+
5878
+ if (openConversation) {
5879
+ this.messengerState.setActiveConversation(openConversation.id);
5880
+ await this._handleSendMessage(
5881
+ openConversation.id,
5882
+ { content: messageContent },
5883
+ pendingAttachments
5884
+ );
5885
+ return openConversation;
5886
+ }
5887
+
5888
+ return await this.startNewConversation(
5889
+ messageContent,
5890
+ '',
5891
+ pendingAttachments
5892
+ );
5045
5893
  } catch (error) {
5046
5894
  console.error('[MessengerWidget] Failed to start conversation:', error);
5895
+ return null;
5047
5896
  }
5048
5897
  }
5049
5898
 
5050
- /**
5051
- * Handle selecting a conversation from the list
5052
- */
5053
5899
  async _handleSelectConversation(conversationId) {
5054
5900
  try {
5055
- await this.fetchMessages(conversationId);
5901
+ await this.fetchMessages(conversationId);
5902
+ } catch (error) {
5903
+ console.error('[MessengerWidget] Failed to fetch messages:', error);
5904
+ }
5905
+ }
5906
+
5907
+ _handleNewConversationClick() {
5908
+ const openConversation = this.messengerState.conversations.find(
5909
+ (c) => c.status === 'open'
5910
+ );
5911
+
5912
+ if (openConversation) {
5913
+ this.messengerState.setActiveConversation(openConversation.id);
5914
+ this.messengerState.setView('chat');
5915
+ this._handleSelectConversation(openConversation.id);
5916
+ } else {
5917
+ this.messengerState.setActiveConversation(null);
5918
+ this.messengerState.setView('chat');
5919
+ }
5920
+ }
5921
+
5922
+ async _handleIdentifyContact(contactData) {
5923
+ try {
5924
+ const response = await this.apiService.identifyContact({
5925
+ name: contactData.name,
5926
+ email: contactData.email,
5927
+ });
5928
+
5929
+ if (response.status) {
5930
+ console.log('[MessengerWidget] Contact identified:', contactData.email);
5931
+
5932
+ if (!this.messengerState.userContext) {
5933
+ this.messengerState.userContext = {};
5934
+ }
5935
+ this.messengerState.userContext.name = contactData.name;
5936
+ this.messengerState.userContext.email = contactData.email;
5937
+
5938
+ const pendingMessage = this.messengerState.pendingMessage;
5939
+ if (pendingMessage) {
5940
+ this.messengerState.pendingMessage = null;
5941
+
5942
+ await this.startNewConversation(
5943
+ pendingMessage.content,
5944
+ '',
5945
+ pendingMessage.attachments
5946
+ );
5947
+ } else {
5948
+ this.messengerState.setView('chat');
5949
+ }
5950
+ }
5951
+
5952
+ return response;
5953
+ } catch (error) {
5954
+ console.error('[MessengerWidget] Failed to identify contact:', error);
5955
+ throw error;
5956
+ }
5957
+ }
5958
+
5959
+ async _handleUploadFile(base64Data, filename) {
5960
+ try {
5961
+ const response = await this.apiService.uploadFile(base64Data, filename);
5962
+ if (response.status && response.url) {
5963
+ return response.url;
5964
+ }
5965
+ throw new Error('Upload failed');
5056
5966
  } catch (error) {
5057
- console.error('[MessengerWidget] Failed to fetch messages:', error);
5967
+ console.error('[MessengerWidget] Failed to upload file:', error);
5968
+ throw error;
5058
5969
  }
5059
5970
  }
5060
5971
 
5061
- /**
5062
- * Handle clicking "new conversation" button
5063
- */
5064
- _handleNewConversationClick() {
5065
- // View is already changed by ConversationsView
5066
- // This is for any additional setup needed
5972
+ async _uploadPendingAttachments(pendingAttachments) {
5973
+ if (!pendingAttachments || pendingAttachments.length === 0) return [];
5974
+
5975
+ const uploaded = [];
5976
+ for (const att of pendingAttachments) {
5977
+ try {
5978
+ const cdnUrl = await this._handleUploadFile(att.preview, att.file.name);
5979
+ uploaded.push({
5980
+ url: cdnUrl,
5981
+ type: att.type.startsWith('image') ? 'image' : 'file',
5982
+ name: att.file.name,
5983
+ });
5984
+ } catch (err) {
5985
+ console.error(
5986
+ '[MessengerWidget] Skipping failed attachment upload:',
5987
+ att.file.name,
5988
+ err
5989
+ );
5990
+ }
5991
+ }
5992
+ return uploaded;
5067
5993
  }
5068
5994
 
5069
- async _handleSendMessage(conversationId, message) {
5070
- // Emit event for external listeners
5995
+ async _handleSendMessage(conversationId, message, pendingAttachments) {
5071
5996
  this.sdk.eventBus.emit('messenger:messageSent', {
5072
5997
  widget: this,
5073
5998
  conversationId,
@@ -5075,18 +6000,18 @@
5075
6000
  });
5076
6001
 
5077
6002
  try {
5078
- // Send message through API
6003
+ const uploadedAttachments =
6004
+ await this._uploadPendingAttachments(pendingAttachments);
6005
+
5079
6006
  const response = await this.apiService.sendMessage(conversationId, {
5080
6007
  content: message.content,
6008
+ attachments: uploadedAttachments,
5081
6009
  });
5082
6010
 
5083
6011
  if (response.status && response.data) {
5084
- // Update the message ID with server-assigned ID
5085
- // Message is already added to state optimistically in ChatView
5086
6012
  console.log('[MessengerWidget] Message sent:', response.data.id);
5087
6013
  }
5088
6014
 
5089
- // In mock mode, simulate an agent response after a delay
5090
6015
  if (this.apiService?.mock) {
5091
6016
  setTimeout(() => {
5092
6017
  const mockResponse = {
@@ -5104,32 +6029,38 @@
5104
6029
  }
5105
6030
  } catch (error) {
5106
6031
  console.error('[MessengerWidget] Failed to send message:', error);
5107
- // Could add error handling UI here
5108
6032
  }
5109
6033
  }
5110
6034
 
5111
- /**
5112
- * Handle incoming WebSocket message
5113
- */
5114
6035
  _handleWebSocketMessage(data) {
5115
6036
  const { conversation_id, message } = data;
5116
6037
 
5117
- // Transform message to local format
6038
+ let attachments = [];
6039
+ if (message.attachments) {
6040
+ try {
6041
+ attachments =
6042
+ typeof message.attachments === 'string'
6043
+ ? JSON.parse(message.attachments)
6044
+ : message.attachments;
6045
+ } catch (e) {
6046
+ // ignore
6047
+ }
6048
+ }
6049
+
5118
6050
  const localMessage = {
5119
6051
  id: message.id,
5120
6052
  content: message.content,
5121
6053
  isOwn: message.sender_type === 'customer',
5122
6054
  timestamp: message.created_at,
6055
+ attachments: attachments.length > 0 ? attachments : undefined,
5123
6056
  sender: {
5124
6057
  name: message.sender_name || 'Support',
5125
6058
  avatarUrl: message.sender_avatar || null,
5126
6059
  },
5127
6060
  };
5128
6061
 
5129
- // Add message to state
5130
6062
  this.messengerState.addMessage(conversation_id, localMessage);
5131
6063
 
5132
- // Update unread count if panel is closed or viewing different conversation
5133
6064
  if (
5134
6065
  !this.messengerState.isOpen ||
5135
6066
  this.messengerState.activeConversationId !== conversation_id
@@ -5138,9 +6069,6 @@
5138
6069
  }
5139
6070
  }
5140
6071
 
5141
- /**
5142
- * Handle typing started event
5143
- */
5144
6072
  _handleTypingStarted(data) {
5145
6073
  if (data.is_agent) {
5146
6074
  this.messengerState._notify('typingStarted', {
@@ -5150,18 +6078,20 @@
5150
6078
  }
5151
6079
  }
5152
6080
 
5153
- /**
5154
- * Handle typing stopped event
5155
- */
5156
6081
  _handleTypingStopped(data) {
5157
6082
  this.messengerState._notify('typingStopped', {
5158
6083
  conversationId: data.conversation_id,
5159
6084
  });
5160
6085
  }
5161
6086
 
5162
- /**
5163
- * Update unread count from API
5164
- */
6087
+ _handleConversationClosed(data) {
6088
+ const conversationId =
6089
+ data?.conversation_id || data?.id || data?.conversation?.id;
6090
+ if (!conversationId) return;
6091
+
6092
+ this.messengerState.updateConversation(conversationId, { status: 'closed' });
6093
+ }
6094
+
5165
6095
  async _updateUnreadCount() {
5166
6096
  try {
5167
6097
  const response = await this.apiService.getUnreadCount();
@@ -5176,9 +6106,6 @@
5176
6106
  }
5177
6107
  }
5178
6108
 
5179
- /**
5180
- * Initialize WebSocket connection
5181
- */
5182
6109
  _initWebSocket() {
5183
6110
  if (this.wsService) {
5184
6111
  this.wsService.disconnect();
@@ -5191,7 +6118,6 @@
5191
6118
  mock: this.apiService.mock,
5192
6119
  });
5193
6120
 
5194
- // Subscribe to WebSocket events
5195
6121
  this._wsUnsubscribers.push(
5196
6122
  this.wsService.on('message', this._handleWebSocketMessage)
5197
6123
  );
@@ -5201,9 +6127,17 @@
5201
6127
  this._wsUnsubscribers.push(
5202
6128
  this.wsService.on('typing_stopped', this._handleTypingStopped)
5203
6129
  );
6130
+ this._wsUnsubscribers.push(
6131
+ this.wsService.on('conversation_closed', this._handleConversationClosed)
6132
+ );
5204
6133
  this._wsUnsubscribers.push(
5205
6134
  this.wsService.on('connected', () => {
5206
6135
  console.log('[MessengerWidget] WebSocket connected');
6136
+ if (this.messengerState.activeConversationId) {
6137
+ this.wsService.send('conversation:subscribe', {
6138
+ conversation_id: this.messengerState.activeConversationId,
6139
+ });
6140
+ }
5207
6141
  })
5208
6142
  );
5209
6143
  this._wsUnsubscribers.push(
@@ -5212,34 +6146,21 @@
5212
6146
  })
5213
6147
  );
5214
6148
 
5215
- // Connect
5216
6149
  this.wsService.connect();
5217
6150
  }
5218
6151
 
5219
- /**
5220
- * Open the messenger panel
5221
- */
5222
6152
  open() {
5223
6153
  this.messengerState.setOpen(true);
5224
6154
  }
5225
6155
 
5226
- /**
5227
- * Close the messenger panel
5228
- */
5229
6156
  close() {
5230
6157
  this.messengerState.setOpen(false);
5231
6158
  }
5232
6159
 
5233
- /**
5234
- * Toggle the messenger panel
5235
- */
5236
6160
  toggle() {
5237
6161
  this.messengerState.setOpen(!this.messengerState.isOpen);
5238
6162
  }
5239
6163
 
5240
- /**
5241
- * Navigate to a specific view
5242
- */
5243
6164
  navigateTo(view) {
5244
6165
  this.messengerState.setView(view);
5245
6166
  if (!this.messengerState.isOpen) {
@@ -5247,52 +6168,31 @@
5247
6168
  }
5248
6169
  }
5249
6170
 
5250
- /**
5251
- * Set conversations
5252
- */
5253
6171
  setConversations(conversations) {
5254
6172
  this.messengerState.setConversations(conversations);
5255
6173
  }
5256
6174
 
5257
- /**
5258
- * Add a message to a conversation
5259
- */
5260
6175
  addMessage(conversationId, message) {
5261
6176
  this.messengerState.addMessage(conversationId, message);
5262
6177
  }
5263
6178
 
5264
- /**
5265
- * Set help articles
5266
- */
5267
6179
  setHelpArticles(articles) {
5268
6180
  this.messengerState.setHelpArticles(articles);
5269
6181
  }
5270
6182
 
5271
- /**
5272
- * Set home changelog items
5273
- */
5274
6183
  setHomeChangelogItems(items) {
5275
6184
  this.messengerState.setHomeChangelogItems(items);
5276
6185
  }
5277
6186
 
5278
- /**
5279
- * Set changelog items
5280
- */
5281
6187
  setChangelogItems(items) {
5282
6188
  this.messengerState.setChangelogItems(items);
5283
6189
  }
5284
6190
 
5285
- /**
5286
- * Update unread count (for external updates)
5287
- */
5288
6191
  setUnreadCount(count) {
5289
6192
  this.messengerState.unreadCount = count;
5290
6193
  this.messengerState._notify('unreadCountChange', { count });
5291
6194
  }
5292
6195
 
5293
- /**
5294
- * Get current state
5295
- */
5296
6196
  getState() {
5297
6197
  return {
5298
6198
  isOpen: this.messengerState.isOpen,
@@ -5302,11 +6202,7 @@
5302
6202
  };
5303
6203
  }
5304
6204
 
5305
- /**
5306
- * Load initial data (mock or API)
5307
- */
5308
6205
  async loadInitialData() {
5309
- // Load conversations
5310
6206
  try {
5311
6207
  const conversations = await this._fetchConversations();
5312
6208
  this.messengerState.setConversations(conversations);
@@ -5314,7 +6210,6 @@
5314
6210
  console.error('[MessengerWidget] Failed to load conversations:', error);
5315
6211
  }
5316
6212
 
5317
- // Load help articles if enabled
5318
6213
  if (this.messengerOptions.enableHelp) {
5319
6214
  try {
5320
6215
  const articles = await this._fetchHelpArticles();
@@ -5324,7 +6219,6 @@
5324
6219
  }
5325
6220
  }
5326
6221
 
5327
- // Load changelog if enabled
5328
6222
  if (this.messengerOptions.enableChangelog) {
5329
6223
  try {
5330
6224
  const { homeItems, changelogItems } = await this._fetchChangelog();
@@ -5340,7 +6234,6 @@
5340
6234
  try {
5341
6235
  const response = await this.apiService.getConversations();
5342
6236
  if (response.status && response.data) {
5343
- // Transform API response to local format
5344
6237
  return response.data.map((conv) => ({
5345
6238
  id: conv.id,
5346
6239
  title:
@@ -5371,7 +6264,6 @@
5371
6264
  try {
5372
6265
  const response = await this.apiService.getHelpCollections();
5373
6266
  if (response.status && response.data) {
5374
- // Transform API response to local format
5375
6267
  return response.data.map((collection) => ({
5376
6268
  id: collection.id,
5377
6269
  title: collection.title || collection.name,
@@ -5389,28 +6281,39 @@
5389
6281
  }
5390
6282
  }
5391
6283
 
5392
- /**
5393
- * Fetch messages for a conversation
5394
- */
5395
6284
  async fetchMessages(conversationId) {
5396
6285
  try {
5397
6286
  const response = await this.apiService.getConversation(conversationId);
5398
6287
  if (response.status && response.data) {
5399
- const messages = (response.data.messages || []).map((msg) => ({
5400
- id: msg.id,
5401
- content: msg.content,
5402
- isOwn: msg.sender_type === 'customer',
5403
- timestamp: msg.created_at,
5404
- sender: {
5405
- name:
5406
- msg.sender_name ||
5407
- (msg.sender_type === 'customer' ? 'You' : 'Support'),
5408
- avatarUrl: msg.sender_avatar || null,
5409
- },
5410
- }));
6288
+ const messages = (response.data.messages || []).map((msg) => {
6289
+ let attachments;
6290
+ if (msg.attachments) {
6291
+ try {
6292
+ attachments =
6293
+ typeof msg.attachments === 'string'
6294
+ ? JSON.parse(msg.attachments)
6295
+ : msg.attachments;
6296
+ } catch (e) {
6297
+ // ignore
6298
+ }
6299
+ }
6300
+ return {
6301
+ id: msg.id,
6302
+ content: msg.content,
6303
+ isOwn: msg.sender_type === 'customer',
6304
+ timestamp: msg.created_at,
6305
+ attachments:
6306
+ attachments && attachments.length > 0 ? attachments : undefined,
6307
+ sender: {
6308
+ name:
6309
+ msg.sender_name ||
6310
+ (msg.sender_type === 'customer' ? 'You' : 'Support'),
6311
+ avatarUrl: msg.sender_avatar || null,
6312
+ },
6313
+ };
6314
+ });
5411
6315
  this.messengerState.setMessages(conversationId, messages);
5412
6316
 
5413
- // Mark as read
5414
6317
  await this.apiService.markConversationAsRead(conversationId);
5415
6318
  this.messengerState.markAsRead(conversationId);
5416
6319
 
@@ -5423,16 +6326,30 @@
5423
6326
  }
5424
6327
  }
5425
6328
 
5426
- /**
5427
- * Start a new conversation
5428
- */
5429
- async startNewConversation(message, subject = '') {
6329
+ async startNewConversation(message, subject = '', pendingAttachments = []) {
5430
6330
  try {
6331
+ const uploadedAttachments =
6332
+ await this._uploadPendingAttachments(pendingAttachments);
6333
+
6334
+ console.log('[MessengerWidget] Starting conversation...', {
6335
+ message,
6336
+ attachmentCount: uploadedAttachments.length,
6337
+ hasSession: this.apiService.isSessionValid(),
6338
+ sessionToken: this.apiService.sessionToken
6339
+ ? this.apiService.sessionToken.substring(0, 10) + '...'
6340
+ : null,
6341
+ baseURL: this.apiService.baseURL,
6342
+ mock: this.apiService.mock,
6343
+ });
6344
+
5431
6345
  const response = await this.apiService.startConversation({
5432
6346
  message,
5433
6347
  subject,
6348
+ attachments: uploadedAttachments,
5434
6349
  });
5435
6350
 
6351
+ console.log('[MessengerWidget] Conversation response:', response);
6352
+
5436
6353
  if (response.status && response.data) {
5437
6354
  const conv = response.data;
5438
6355
  const newConversation = {
@@ -5445,10 +6362,8 @@
5445
6362
  status: 'open',
5446
6363
  };
5447
6364
 
5448
- // Add to state
5449
6365
  this.messengerState.addConversation(newConversation);
5450
6366
 
5451
- // Set initial message in messages cache
5452
6367
  this.messengerState.setMessages(conv.id, [
5453
6368
  {
5454
6369
  id: 'msg_' + Date.now(),
@@ -5458,7 +6373,6 @@
5458
6373
  },
5459
6374
  ]);
5460
6375
 
5461
- // Navigate to chat
5462
6376
  this.messengerState.setActiveConversation(conv.id);
5463
6377
  this.messengerState.setView('chat');
5464
6378
 
@@ -5471,20 +6385,14 @@
5471
6385
  }
5472
6386
  }
5473
6387
 
5474
- /**
5475
- * Send typing indicator
5476
- */
5477
6388
  async sendTypingIndicator(conversationId, isTyping) {
5478
6389
  try {
5479
6390
  await this.apiService.sendTypingIndicator(conversationId, isTyping);
5480
6391
  } catch (error) {
5481
- // Silently fail - typing indicators are not critical
6392
+ // Silent fail
5482
6393
  }
5483
6394
  }
5484
6395
 
5485
- /**
5486
- * Check agent availability
5487
- */
5488
6396
  async checkAgentAvailability() {
5489
6397
  try {
5490
6398
  const response = await this.apiService.checkAgentsOnline();
@@ -5492,6 +6400,13 @@
5492
6400
  this.messengerState.agentsOnline = response.data.agents_online;
5493
6401
  this.messengerState.onlineCount = response.data.online_count || 0;
5494
6402
  this.messengerState.responseTime = response.data.response_time || '';
6403
+
6404
+ if (response.data.available_agents) {
6405
+ this.messengerState.setTeamAvatarsFromAgents(
6406
+ response.data.available_agents
6407
+ );
6408
+ }
6409
+
5495
6410
  this.messengerState._notify('availabilityUpdate', response.data);
5496
6411
  return response.data;
5497
6412
  }
@@ -5581,11 +6496,9 @@
5581
6496
  };
5582
6497
  }
5583
6498
 
5584
- // Fetch changelogs from API
5585
6499
  const response = await this.apiService.getChangelogs({ limit: 20 });
5586
6500
  const changelogs = response.data || [];
5587
6501
 
5588
- // Map API response to expected format
5589
6502
  const mappedItems = changelogs.map((item) => ({
5590
6503
  id: item.id,
5591
6504
  title: item.title,
@@ -5603,19 +6516,30 @@
5603
6516
  };
5604
6517
  }
5605
6518
 
5606
- onMount() {
5607
- // Load initial data after mounting
6519
+ async onMount() {
6520
+ const userContext = this.messengerState.userContext;
6521
+ if (userContext?.email && userContext?.name) {
6522
+ try {
6523
+ await this.apiService.identifyContact({
6524
+ name: userContext.name,
6525
+ email: userContext.email,
6526
+ });
6527
+ console.log('[MessengerWidget] User identified successfully');
6528
+ } catch (error) {
6529
+ if (error?.code !== 'ALREADY_IDENTIFIED') {
6530
+ console.warn('[MessengerWidget] Identification failed:', error);
6531
+ }
6532
+ }
6533
+ }
6534
+
5608
6535
  this.loadInitialData();
5609
6536
 
5610
- // Initialize WebSocket for real-time updates
5611
6537
  if (this.apiService?.sessionToken) {
5612
6538
  this._initWebSocket();
5613
6539
  }
5614
6540
 
5615
- // Check agent availability
5616
6541
  this.checkAgentAvailability();
5617
6542
 
5618
- // Periodically check availability (every 60 seconds)
5619
6543
  this._availabilityInterval = setInterval(() => {
5620
6544
  this.checkAgentAvailability();
5621
6545
  }, 60000);
@@ -5626,16 +6550,13 @@
5626
6550
  this._stateUnsubscribe();
5627
6551
  }
5628
6552
 
5629
- // Clean up WebSocket
5630
6553
  if (this.wsService) {
5631
6554
  this.wsService.disconnect();
5632
6555
  }
5633
6556
 
5634
- // Clean up WebSocket event listeners
5635
6557
  this._wsUnsubscribers.forEach((unsub) => unsub());
5636
6558
  this._wsUnsubscribers = [];
5637
6559
 
5638
- // Clean up availability interval
5639
6560
  if (this._availabilityInterval) {
5640
6561
  clearInterval(this._availabilityInterval);
5641
6562
  }
@@ -6404,8 +7325,11 @@
6404
7325
  this.apiService = new APIService({
6405
7326
  apiUrl: this.config.apiUrl,
6406
7327
  workspace: this.config.workspace,
7328
+ siteId: this.config.siteId,
7329
+ sessionToken: this.config.sessionToken,
6407
7330
  userContext: this.config.userContext,
6408
7331
  mock: this.config.mock,
7332
+ debug: this.config.debug,
6409
7333
  env: this.config.env,
6410
7334
  });
6411
7335
 
@@ -7201,6 +8125,39 @@
7201
8125
  opacity: 0.6;
7202
8126
  }
7203
8127
 
8128
+ /* Continue conversation variant */
8129
+ .messenger-home-continue-btn {
8130
+ flex-direction: column;
8131
+ align-items: flex-start;
8132
+ gap: 2px;
8133
+ position: relative;
8134
+ }
8135
+
8136
+ .messenger-home-continue-btn > i {
8137
+ position: absolute;
8138
+ right: 20px;
8139
+ top: 50%;
8140
+ transform: translateY(-50%);
8141
+ }
8142
+
8143
+ .messenger-home-continue-info {
8144
+ display: flex;
8145
+ flex-direction: column;
8146
+ gap: 2px;
8147
+ text-align: left;
8148
+ }
8149
+
8150
+ .messenger-home-continue-label {
8151
+ font-size: 14px;
8152
+ font-weight: 600;
8153
+ }
8154
+
8155
+ .messenger-home-continue-preview {
8156
+ font-size: 12px;
8157
+ opacity: 0.6;
8158
+ font-weight: 400;
8159
+ }
8160
+
7204
8161
  /* Featured Card */
7205
8162
  .messenger-home-featured {
7206
8163
  background: #2c2c2e;
@@ -7329,7 +8286,7 @@
7329
8286
  .messenger-conversations-body {
7330
8287
  flex: 1;
7331
8288
  overflow-y: auto;
7332
- padding: 12px;
8289
+ padding: 4px 12px 12px 12px;
7333
8290
  }
7334
8291
 
7335
8292
  .messenger-conversations-empty {
@@ -7364,7 +8321,7 @@
7364
8321
  display: flex;
7365
8322
  align-items: flex-start;
7366
8323
  gap: 12px;
7367
- padding: 16px;
8324
+ padding: 10px 16px;
7368
8325
  border-radius: 12px;
7369
8326
  cursor: pointer;
7370
8327
  transition: background 0.2s ease;
@@ -7496,6 +8453,11 @@
7496
8453
  color: white;
7497
8454
  }
7498
8455
 
8456
+ .messenger-chat-view {
8457
+ position: relative;
8458
+ overflow: visible;
8459
+ }
8460
+
7499
8461
  .messenger-chat-messages {
7500
8462
  flex: 1;
7501
8463
  overflow-y: auto;
@@ -7570,7 +8532,7 @@
7570
8532
  }
7571
8533
 
7572
8534
  .messenger-message-own .messenger-message-bubble {
7573
- background: #007aff;
8535
+ background: rgb(29, 78, 216);
7574
8536
  color: white;
7575
8537
  border-bottom-right-radius: 4px;
7576
8538
  }
@@ -7598,10 +8560,25 @@
7598
8560
  margin-top: auto;
7599
8561
  }
7600
8562
 
8563
+ /* Conversation Closed Banner */
8564
+ .messenger-closed-banner {
8565
+ display: flex;
8566
+ align-items: center;
8567
+ justify-content: center;
8568
+ gap: 8px;
8569
+ padding: 12px 16px;
8570
+ margin: 16px;
8571
+ background: rgba(52, 199, 89, 0.12);
8572
+ color: #34c759;
8573
+ border-radius: 12px;
8574
+ font-size: 13px;
8575
+ font-weight: 500;
8576
+ }
8577
+
7601
8578
  /* Compose Area */
7602
8579
  .messenger-chat-compose {
7603
8580
  display: flex;
7604
- align-items: flex-end;
8581
+ align-items: center;
7605
8582
  gap: 8px;
7606
8583
  padding: 12px 16px;
7607
8584
  border-top: 1px solid rgba(255, 255, 255, 0.1);
@@ -7611,7 +8588,7 @@
7611
8588
  .messenger-compose-input-wrapper {
7612
8589
  flex: 1;
7613
8590
  background: #2c2c2e;
7614
- border-radius: 20px;
8591
+ border-radius: 10px;
7615
8592
  padding: 8px 16px;
7616
8593
  }
7617
8594
 
@@ -7647,15 +8624,176 @@
7647
8624
  flex-shrink: 0;
7648
8625
  }
7649
8626
 
7650
- .messenger-compose-send:hover:not(:disabled) {
7651
- background: #0066d6;
7652
- transform: scale(1.05);
8627
+ .messenger-compose-send:hover:not(:disabled) {
8628
+ background: #0066d6;
8629
+ transform: scale(1.05);
8630
+ }
8631
+
8632
+ .messenger-compose-send:disabled {
8633
+ background: #3c3c3e;
8634
+ color: rgba(255, 255, 255, 0.3);
8635
+ cursor: not-allowed;
8636
+ }
8637
+
8638
+ /* Attach Button */
8639
+ .messenger-compose-attach {
8640
+ width: 40px;
8641
+ height: 40px;
8642
+ background: transparent;
8643
+ border: none;
8644
+ border-radius: 50%;
8645
+ color: rgba(255, 255, 255, 0.5);
8646
+ cursor: pointer;
8647
+ display: flex;
8648
+ align-items: center;
8649
+ justify-content: center;
8650
+ transition: all 0.2s ease;
8651
+ flex-shrink: 0;
8652
+ }
8653
+
8654
+ .messenger-compose-attach:hover:not(:disabled) {
8655
+ color: rgba(255, 255, 255, 0.85);
8656
+ background: rgba(255, 255, 255, 0.08);
8657
+ }
8658
+
8659
+ .messenger-compose-attach:disabled {
8660
+ opacity: 0.3;
8661
+ cursor: not-allowed;
8662
+ }
8663
+
8664
+ /* Attachment Preview Strip */
8665
+ .messenger-compose-attachments-preview {
8666
+ display: none;
8667
+ flex-wrap: wrap;
8668
+ gap: 8px;
8669
+ padding: 8px 16px;
8670
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
8671
+ background: #1c1c1e;
8672
+ }
8673
+
8674
+ .messenger-attachment-preview {
8675
+ position: relative;
8676
+ width: 56px;
8677
+ height: 56px;
8678
+ border-radius: 8px;
8679
+ overflow: hidden;
8680
+ border: 1px solid rgba(255, 255, 255, 0.15);
8681
+ }
8682
+
8683
+ .messenger-attachment-thumb {
8684
+ width: 100%;
8685
+ height: 100%;
8686
+ object-fit: cover;
8687
+ display: block;
8688
+ }
8689
+
8690
+ .messenger-attachment-file-icon {
8691
+ display: flex;
8692
+ align-items: center;
8693
+ justify-content: center;
8694
+ background: #2c2c2e;
8695
+ color: rgba(255, 255, 255, 0.5);
8696
+ }
8697
+
8698
+ .messenger-attachment-remove {
8699
+ position: absolute;
8700
+ top: 2px;
8701
+ right: 2px;
8702
+ width: 18px;
8703
+ height: 18px;
8704
+ background: rgba(0, 0, 0, 0.7);
8705
+ border: none;
8706
+ border-radius: 50%;
8707
+ color: white;
8708
+ font-size: 12px;
8709
+ line-height: 1;
8710
+ cursor: pointer;
8711
+ display: flex;
8712
+ align-items: center;
8713
+ justify-content: center;
8714
+ padding: 0;
8715
+ transition: background 0.15s ease;
8716
+ }
8717
+
8718
+ .messenger-attachment-remove:hover {
8719
+ background: rgba(255, 59, 48, 0.85);
8720
+ }
8721
+
8722
+ /* Message Attachments (inline images & file links) */
8723
+ .messenger-message-image {
8724
+ max-width: 220px;
8725
+ max-height: 200px;
8726
+ width: auto;
8727
+ height: auto;
8728
+ border-radius: 8px;
8729
+ margin-top: 4px;
8730
+ cursor: pointer;
8731
+ object-fit: contain;
8732
+ display: block;
8733
+ }
8734
+
8735
+ .messenger-message-file {
8736
+ display: inline-flex;
8737
+ align-items: center;
8738
+ gap: 6px;
8739
+ margin-top: 4px;
8740
+ padding: 8px 12px;
8741
+ border-radius: 8px;
8742
+ background: #2c2c2e;
8743
+ color: #60a5fa;
8744
+ text-decoration: none;
8745
+ font-size: 13px;
8746
+ transition: background 0.15s ease;
8747
+ max-width: 100%;
8748
+ word-break: break-all;
8749
+ cursor: pointer;
8750
+ }
8751
+
8752
+ .messenger-message-file:hover {
8753
+ background: #3c3c3e;
8754
+ }
8755
+
8756
+ .messenger-file-download-icon {
8757
+ margin-left: auto;
8758
+ opacity: 0.5;
8759
+ flex-shrink: 0;
8760
+ }
8761
+
8762
+ .messenger-message-file:hover .messenger-file-download-icon {
8763
+ opacity: 1;
8764
+ }
8765
+
8766
+ /* Light theme overrides for attachments */
8767
+ .theme-light .messenger-compose-attach {
8768
+ color: #9ca3af;
8769
+ }
8770
+
8771
+ .theme-light .messenger-compose-attach:hover:not(:disabled) {
8772
+ color: #374151;
8773
+ background: #f3f4f6;
8774
+ }
8775
+
8776
+ .theme-light .messenger-compose-attachments-preview {
8777
+ background: #ffffff;
8778
+ border-top-color: #e5e7eb;
8779
+ }
8780
+
8781
+ .theme-light .messenger-attachment-preview {
8782
+ border-color: #e5e7eb;
8783
+ }
8784
+
8785
+ .theme-light .messenger-attachment-file-icon {
8786
+ background: #f3f4f6;
8787
+ color: #6b7280;
8788
+ }
8789
+
8790
+ .theme-light .messenger-message-file {
8791
+ background: #f3f4f6;
8792
+ color: #2563eb;
7653
8793
  }
7654
8794
 
7655
- .messenger-compose-send:disabled {
7656
- background: #3c3c3e;
7657
- color: rgba(255, 255, 255, 0.3);
7658
- cursor: not-allowed;
8795
+ .theme-light .messenger-message-file:hover {
8796
+ background: #e5e7eb;
7659
8797
  }
7660
8798
 
7661
8799
  /* ========================================
@@ -7972,7 +9110,7 @@
7972
9110
 
7973
9111
  .messenger-nav {
7974
9112
  display: flex;
7975
- padding: 8px 12px;
9113
+ padding: 4px 8px;
7976
9114
  gap: 4px;
7977
9115
  }
7978
9116
 
@@ -8025,8 +9163,8 @@
8025
9163
  }
8026
9164
 
8027
9165
  .messenger-nav-label {
8028
- font-size: 14px;
8029
- font-weight: 400;
9166
+ font-size: 11px;
9167
+ font-weight: 500;
8030
9168
  color: rgba(255, 255, 255, 0.5);
8031
9169
  transition: color 0.2s ease;
8032
9170
  }
@@ -8285,7 +9423,7 @@
8285
9423
  }
8286
9424
 
8287
9425
  .theme-light .messenger-message-own .messenger-message-bubble {
8288
- background: #007aff;
9426
+ background: rgb(29, 78, 216);
8289
9427
  color: #ffffff;
8290
9428
  }
8291
9429
 
@@ -8293,6 +9431,11 @@
8293
9431
  color: #86868b;
8294
9432
  }
8295
9433
 
9434
+ .theme-light .messenger-closed-banner {
9435
+ background: rgba(52, 199, 89, 0.1);
9436
+ color: #22883a;
9437
+ }
9438
+
8296
9439
  .theme-light .messenger-chat-compose {
8297
9440
  background: #ffffff;
8298
9441
  border-top-color: #e5e5e7;
@@ -8559,6 +9702,191 @@
8559
9702
  }
8560
9703
  }
8561
9704
 
9705
+ /* ========================================
9706
+ Pre-Chat Form View (Transparent Overlay)
9707
+ ======================================== */
9708
+
9709
+ .messenger-prechat-view {
9710
+ background: transparent;
9711
+ position: relative;
9712
+ }
9713
+
9714
+ .messenger-prechat-overlay {
9715
+ position: absolute;
9716
+ top: 0;
9717
+ left: 0;
9718
+ right: 0;
9719
+ bottom: 0;
9720
+ background: rgba(0, 0, 0, 0.5);
9721
+ backdrop-filter: blur(2px);
9722
+ display: flex;
9723
+ align-items: flex-end;
9724
+ padding: 16px;
9725
+ animation: messenger-fade-in 0.2s ease;
9726
+ }
9727
+
9728
+ .messenger-prechat-card {
9729
+ background: #1c1c1e;
9730
+ border-radius: 16px;
9731
+ padding: 20px;
9732
+ width: 100%;
9733
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
9734
+ animation: messenger-slide-up 0.25s ease;
9735
+ }
9736
+
9737
+ .messenger-prechat-card h4 {
9738
+ margin: 0 0 14px;
9739
+ font-size: 15px;
9740
+ font-weight: 600;
9741
+ color: white;
9742
+ text-align: center;
9743
+ }
9744
+
9745
+ .messenger-prechat-form {
9746
+ display: flex;
9747
+ flex-direction: column;
9748
+ gap: 10px;
9749
+ }
9750
+
9751
+ .messenger-prechat-fields {
9752
+ display: flex;
9753
+ flex-direction: column;
9754
+ gap: 8px;
9755
+ }
9756
+
9757
+ .messenger-prechat-input {
9758
+ width: 100%;
9759
+ padding: 11px 14px;
9760
+ background: #2c2c2e;
9761
+ border: 1px solid rgba(255, 255, 255, 0.1);
9762
+ border-radius: 10px;
9763
+ color: white;
9764
+ font-size: 14px;
9765
+ font-family: inherit;
9766
+ outline: none;
9767
+ transition: border-color 0.2s ease;
9768
+ }
9769
+
9770
+ .messenger-prechat-input:focus {
9771
+ border-color: #007aff;
9772
+ }
9773
+
9774
+ .messenger-prechat-input::placeholder {
9775
+ color: rgba(255, 255, 255, 0.4);
9776
+ }
9777
+
9778
+ .messenger-prechat-error {
9779
+ font-size: 12px;
9780
+ color: #ef4444;
9781
+ display: none;
9782
+ text-align: center;
9783
+ }
9784
+
9785
+ .messenger-prechat-actions {
9786
+ display: flex;
9787
+ gap: 10px;
9788
+ margin-top: 4px;
9789
+ }
9790
+
9791
+ .messenger-prechat-skip {
9792
+ flex: 1;
9793
+ padding: 11px 14px;
9794
+ background: transparent;
9795
+ border: 1px solid rgba(255, 255, 255, 0.2);
9796
+ border-radius: 20px;
9797
+ color: rgba(255, 255, 255, 0.7);
9798
+ font-size: 14px;
9799
+ font-weight: 500;
9800
+ cursor: pointer;
9801
+ transition: all 0.2s ease;
9802
+ }
9803
+
9804
+ .messenger-prechat-skip:hover {
9805
+ background: rgba(255, 255, 255, 0.05);
9806
+ border-color: rgba(255, 255, 255, 0.3);
9807
+ }
9808
+
9809
+ .messenger-prechat-submit {
9810
+ flex: 1;
9811
+ display: flex;
9812
+ align-items: center;
9813
+ justify-content: center;
9814
+ gap: 6px;
9815
+ padding: 11px 14px;
9816
+ background: #007aff;
9817
+ border: none;
9818
+ border-radius: 20px;
9819
+ color: white;
9820
+ font-size: 14px;
9821
+ font-weight: 600;
9822
+ cursor: pointer;
9823
+ transition: all 0.2s ease;
9824
+ }
9825
+
9826
+ .messenger-prechat-submit:hover:not(:disabled) {
9827
+ background: #0066d6;
9828
+ }
9829
+
9830
+ .messenger-prechat-submit:disabled {
9831
+ background: #3c3c3e;
9832
+ color: rgba(255, 255, 255, 0.4);
9833
+ cursor: not-allowed;
9834
+ }
9835
+
9836
+ .messenger-prechat-submit-loading {
9837
+ display: inline-flex;
9838
+ align-items: center;
9839
+ animation: messenger-spin 1s linear infinite;
9840
+ }
9841
+
9842
+ @keyframes messenger-spin {
9843
+ from { transform: rotate(0deg); }
9844
+ to { transform: rotate(360deg); }
9845
+ }
9846
+
9847
+ /* Light Theme - Pre-Chat Form */
9848
+ .theme-light .messenger-prechat-overlay {
9849
+ background: rgba(255, 255, 255, 0.6);
9850
+ }
9851
+
9852
+ .theme-light .messenger-prechat-card {
9853
+ background: #ffffff;
9854
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
9855
+ }
9856
+
9857
+ .theme-light .messenger-prechat-card h4 {
9858
+ color: #1d1d1f;
9859
+ }
9860
+
9861
+ .theme-light .messenger-prechat-input {
9862
+ background: #f5f5f7;
9863
+ border-color: #e5e5e7;
9864
+ color: #1d1d1f;
9865
+ }
9866
+
9867
+ .theme-light .messenger-prechat-input:focus {
9868
+ border-color: #007aff;
9869
+ background: #ffffff;
9870
+ }
9871
+
9872
+ .theme-light .messenger-prechat-input::placeholder {
9873
+ color: #86868b;
9874
+ }
9875
+
9876
+ .theme-light .messenger-prechat-skip {
9877
+ border-color: #e5e5e7;
9878
+ color: #6e6e73;
9879
+ }
9880
+
9881
+ .theme-light .messenger-prechat-skip:hover {
9882
+ background: #f5f5f7;
9883
+ }
9884
+
9885
+ .theme-light .messenger-prechat-submit:disabled {
9886
+ background: #e5e5e7;
9887
+ color: #c7c7cc;
9888
+ }
9889
+
8562
9890
  /* ========================================
8563
9891
  Animations
8564
9892
  ======================================== */
@@ -8590,6 +9918,173 @@
8590
9918
  transition: none;
8591
9919
  }
8592
9920
  }
9921
+
9922
+ /* ========================================
9923
+ Email Collection Overlay (Bottom Sheet)
9924
+ ======================================== */
9925
+
9926
+ .messenger-email-overlay {
9927
+ position: absolute;
9928
+ bottom: -56px;
9929
+ left: 0;
9930
+ right: 0;
9931
+ top: 0;
9932
+ display: flex;
9933
+ align-items: flex-end;
9934
+ z-index: 20;
9935
+ background: rgba(0, 0, 0, 0.08);
9936
+ pointer-events: auto;
9937
+ }
9938
+
9939
+ .messenger-email-card {
9940
+ width: 100%;
9941
+ background: #ffffff;
9942
+ border-radius: 0;
9943
+ padding: 16px 16px 72px;
9944
+ box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.08);
9945
+ animation: messenger-slide-up 0.25s ease;
9946
+ }
9947
+
9948
+ .messenger-email-card h4 {
9949
+ margin: 0 0 2px;
9950
+ font-size: 13px;
9951
+ font-weight: 600;
9952
+ color: #1d1d1f;
9953
+ text-align: center;
9954
+ }
9955
+
9956
+ .messenger-email-card p {
9957
+ margin: 0 0 10px;
9958
+ font-size: 11px;
9959
+ color: #6b7280;
9960
+ text-align: center;
9961
+ }
9962
+
9963
+ .messenger-email-name,
9964
+ .messenger-email-input {
9965
+ width: 100%;
9966
+ padding: 8px 10px;
9967
+ background: #f3f4f6;
9968
+ border: 1px solid transparent;
9969
+ border-radius: 8px;
9970
+ color: #1d1d1f;
9971
+ font-size: 12px;
9972
+ font-family: inherit;
9973
+ outline: none;
9974
+ margin-bottom: 6px;
9975
+ transition: border-color 0.2s ease, box-shadow 0.2s ease;
9976
+ }
9977
+
9978
+ .messenger-email-name:focus,
9979
+ .messenger-email-input:focus {
9980
+ border-color: #007aff;
9981
+ box-shadow: 0 0 0 2px rgba(0, 122, 255, 0.12);
9982
+ background: #ffffff;
9983
+ }
9984
+
9985
+ .messenger-email-name::placeholder,
9986
+ .messenger-email-input::placeholder {
9987
+ color: #9ca3af;
9988
+ }
9989
+
9990
+ .messenger-email-actions {
9991
+ display: flex;
9992
+ gap: 8px;
9993
+ margin-top: 4px;
9994
+ }
9995
+
9996
+ .messenger-email-submit {
9997
+ flex: 1.2;
9998
+ padding: 7px 12px;
9999
+ background: #007aff;
10000
+ border: none;
10001
+ border-radius: 8px;
10002
+ color: white;
10003
+ font-size: 12px;
10004
+ font-weight: 600;
10005
+ cursor: pointer;
10006
+ transition: all 0.2s ease;
10007
+ }
10008
+
10009
+ .messenger-email-submit:hover:not(:disabled) {
10010
+ background: #0066d6;
10011
+ }
10012
+
10013
+ .messenger-email-submit:disabled {
10014
+ background: #d1d5db;
10015
+ color: #9ca3af;
10016
+ cursor: not-allowed;
10017
+ }
10018
+
10019
+ .messenger-email-skip {
10020
+ flex: 0.8;
10021
+ padding: 7px 12px;
10022
+ background: #ffffff;
10023
+ border: 1px solid #e5e5e7;
10024
+ border-radius: 8px;
10025
+ color: #4b5563;
10026
+ font-size: 12px;
10027
+ font-weight: 500;
10028
+ cursor: pointer;
10029
+ transition: all 0.2s ease;
10030
+ }
10031
+
10032
+ .messenger-email-skip:hover {
10033
+ background: #f9fafb;
10034
+ border-color: #d1d5db;
10035
+ }
10036
+
10037
+ /* Dark Theme - Email Overlay */
10038
+ .theme-dark .messenger-email-overlay {
10039
+ background: rgba(0, 0, 0, 0.3);
10040
+ }
10041
+
10042
+ .theme-dark .messenger-email-card {
10043
+ background: #1c1c1e;
10044
+ }
10045
+
10046
+ .theme-dark .messenger-email-card h4 {
10047
+ color: white;
10048
+ }
10049
+
10050
+ .theme-dark .messenger-email-card p {
10051
+ color: rgba(255, 255, 255, 0.6);
10052
+ }
10053
+
10054
+ .theme-dark .messenger-email-name,
10055
+ .theme-dark .messenger-email-input {
10056
+ background: #2c2c2e;
10057
+ border-color: transparent;
10058
+ color: white;
10059
+ }
10060
+
10061
+ .theme-dark .messenger-email-name:focus,
10062
+ .theme-dark .messenger-email-input:focus {
10063
+ border-color: #007aff;
10064
+ box-shadow: 0 0 0 2px rgba(0, 122, 255, 0.25);
10065
+ background: #1c1c1e;
10066
+ }
10067
+
10068
+ .theme-dark .messenger-email-name::placeholder,
10069
+ .theme-dark .messenger-email-input::placeholder {
10070
+ color: rgba(255, 255, 255, 0.4);
10071
+ }
10072
+
10073
+ .theme-dark .messenger-email-submit:disabled {
10074
+ background: #3c3c3e;
10075
+ color: rgba(255, 255, 255, 0.4);
10076
+ }
10077
+
10078
+ .theme-dark .messenger-email-skip {
10079
+ background: transparent;
10080
+ border-color: rgba(255, 255, 255, 0.15);
10081
+ color: rgba(255, 255, 255, 0.8);
10082
+ }
10083
+
10084
+ .theme-dark .messenger-email-skip:hover {
10085
+ background: rgba(255, 255, 255, 0.05);
10086
+ border-color: rgba(255, 255, 255, 0.25);
10087
+ }
8593
10088
  `;
8594
10089
 
8595
10090
  const baseStyles = `
@@ -10029,7 +11524,7 @@
10029
11524
  EventBus,
10030
11525
  APIService,
10031
11526
  SDKError,
10032
- APIError,
11527
+ APIError: APIError$1,
10033
11528
  WidgetError,
10034
11529
  ConfigError,
10035
11530
  ValidationError,
@@ -10099,7 +11594,7 @@
10099
11594
  handleDOMReady();
10100
11595
  }
10101
11596
 
10102
- exports.APIError = APIError;
11597
+ exports.APIError = APIError$1;
10103
11598
  exports.APIService = APIService;
10104
11599
  exports.BaseWidget = BaseWidget;
10105
11600
  exports.ButtonWidget = ButtonWidget;