@product7/feedback-sdk 1.3.0 → 1.3.1

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
 
@@ -4798,6 +5066,37 @@
4798
5066
  `;
4799
5067
  }
4800
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
+
4801
5100
  _renderFeaturedCard() {
4802
5101
  if (!this.options.featuredContent) {
4803
5102
  return '';
@@ -4873,11 +5172,25 @@
4873
5172
  this.state.setOpen(false);
4874
5173
  });
4875
5174
 
4876
- this.element
4877
- .querySelector('.messenger-home-message-btn')
4878
- .addEventListener('click', () => {
4879
- this.state.setView('messages');
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
+ }
4880
5192
  });
5193
+ }
4881
5194
 
4882
5195
  this.element
4883
5196
  .querySelectorAll('.messenger-home-changelog-item')
@@ -4911,15 +5224,516 @@
4911
5224
  });
4912
5225
  }
4913
5226
  }
4914
-
4915
- destroy() {
4916
- if (this._unsubscribe) {
4917
- this._unsubscribe();
4918
- }
4919
- if (this.element && this.element.parentNode) {
4920
- this.element.parentNode.removeChild(this.element);
4921
- }
4922
- }
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;
5649
+
5650
+ if (this.pingInterval) {
5651
+ clearInterval(this.pingInterval);
5652
+ this.pingInterval = null;
5653
+ }
5654
+
5655
+ this._emit('disconnected', { code: event.code, reason: event.reason });
5656
+ this._scheduleReconnect();
5657
+ }
5658
+
5659
+ _onError(error) {
5660
+ console.error('[WebSocket] Error:', error);
5661
+ this._emit('error', { error });
5662
+ }
5663
+
5664
+ _scheduleReconnect() {
5665
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) {
5666
+ console.log('[WebSocket] Max reconnect attempts reached');
5667
+ this._emit('reconnect_failed', {});
5668
+ return;
5669
+ }
5670
+
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
+ );
5676
+
5677
+ setTimeout(() => {
5678
+ this.connect();
5679
+ }, delay);
5680
+ }
5681
+
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
+ }
5690
+ });
5691
+ }
5692
+ }
5693
+
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,
5706
+ });
5707
+
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);
5717
+ }
5718
+
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
+ });
5735
+ }
5736
+ }
4923
5737
  }
4924
5738
 
4925
5739
  /**
@@ -4967,6 +5781,7 @@
4967
5781
  this._handleWebSocketMessage = this._handleWebSocketMessage.bind(this);
4968
5782
  this._handleTypingStarted = this._handleTypingStarted.bind(this);
4969
5783
  this._handleTypingStopped = this._handleTypingStopped.bind(this);
5784
+ this._handleConversationClosed = this._handleConversationClosed.bind(this);
4970
5785
  }
4971
5786
 
4972
5787
  _render() {
@@ -4998,6 +5813,8 @@
4998
5813
  // Conversation list callbacks
4999
5814
  onSelectConversation: this._handleSelectConversation.bind(this),
5000
5815
  onStartNewConversation: this._handleNewConversationClick.bind(this),
5816
+ // Pre-chat form callbacks
5817
+ onIdentifyContact: this._handleIdentifyContact.bind(this),
5001
5818
  // Article/changelog callbacks
5002
5819
  onArticleClick: this.messengerOptions.onArticleClick,
5003
5820
  onChangelogClick: this.messengerOptions.onChangelogClick,
@@ -5007,6 +5824,7 @@
5007
5824
  this.panel.registerView('home', HomeView);
5008
5825
  this.panel.registerView('messages', ConversationsView);
5009
5826
  this.panel.registerView('chat', ChatView);
5827
+ this.panel.registerView('prechat', PreChatFormView);
5010
5828
  this.panel.registerView('help', HelpView);
5011
5829
  this.panel.registerView('changelog', ChangelogView);
5012
5830
 
@@ -5022,6 +5840,9 @@
5022
5840
  if (type === 'openChange') {
5023
5841
  this._handleOpenChange(data.isOpen);
5024
5842
  }
5843
+ if (type === 'conversationChange') {
5844
+ this._handleActiveConversationChange(data.conversationId, data.previousConversationId);
5845
+ }
5025
5846
  });
5026
5847
  }
5027
5848
 
@@ -5036,14 +5857,44 @@
5036
5857
  }
5037
5858
  }
5038
5859
 
5860
+ /**
5861
+ * Subscribe/unsubscribe to conversation WebSocket channel
5862
+ */
5863
+ _handleActiveConversationChange(conversationId, previousConversationId) {
5864
+ if (previousConversationId && this.wsService) {
5865
+ this.wsService.send('conversation:unsubscribe', { conversation_id: previousConversationId });
5866
+ }
5867
+ if (conversationId && this.wsService) {
5868
+ this.wsService.send('conversation:subscribe', { conversation_id: conversationId });
5869
+ }
5870
+ }
5871
+
5039
5872
  /**
5040
5873
  * Handle starting a new conversation
5874
+ * If there's an existing open conversation, send the message there instead
5041
5875
  */
5042
- async _handleStartConversation(messageContent) {
5876
+ async _handleStartConversation(messageContent, pendingAttachments) {
5043
5877
  try {
5044
- await this.startNewConversation(messageContent);
5878
+ // Check for existing open conversation first
5879
+ const openConversation = this.messengerState.conversations.find(
5880
+ (c) => c.status === 'open'
5881
+ );
5882
+
5883
+ if (openConversation) {
5884
+ // Route message to existing open conversation
5885
+ this.messengerState.setActiveConversation(openConversation.id);
5886
+ await this._handleSendMessage(
5887
+ openConversation.id,
5888
+ { content: messageContent },
5889
+ pendingAttachments
5890
+ );
5891
+ return openConversation;
5892
+ }
5893
+
5894
+ return await this.startNewConversation(messageContent, '', pendingAttachments);
5045
5895
  } catch (error) {
5046
5896
  console.error('[MessengerWidget] Failed to start conversation:', error);
5897
+ return null;
5047
5898
  }
5048
5899
  }
5049
5900
 
@@ -5060,13 +5911,88 @@
5060
5911
 
5061
5912
  /**
5062
5913
  * Handle clicking "new conversation" button
5914
+ * Reuses the most recent open conversation if one exists
5063
5915
  */
5064
5916
  _handleNewConversationClick() {
5065
- // View is already changed by ConversationsView
5066
- // This is for any additional setup needed
5917
+ // Check for an existing open conversation to reuse
5918
+ const openConversation = this.messengerState.conversations.find(
5919
+ (c) => c.status === 'open'
5920
+ );
5921
+
5922
+ if (openConversation) {
5923
+ // Reuse existing open conversation
5924
+ this.messengerState.setActiveConversation(openConversation.id);
5925
+ this.messengerState.setView('chat');
5926
+ this._handleSelectConversation(openConversation.id);
5927
+ } else {
5928
+ // No open conversation — start a new one
5929
+ this.messengerState.setActiveConversation(null);
5930
+ this.messengerState.setView('chat');
5931
+ }
5932
+ }
5933
+
5934
+ /**
5935
+ * Handle identifying contact from pre-chat form
5936
+ */
5937
+ async _handleIdentifyContact(contactData) {
5938
+ try {
5939
+ // Call API to identify/update contact
5940
+ const response = await this.apiService.identifyContact({
5941
+ name: contactData.name,
5942
+ email: contactData.email,
5943
+ });
5944
+
5945
+ if (response.status) {
5946
+ console.log('[MessengerWidget] Contact identified:', contactData.email);
5947
+
5948
+ // Update local user context
5949
+ if (!this.messengerState.userContext) {
5950
+ this.messengerState.userContext = {};
5951
+ }
5952
+ this.messengerState.userContext.name = contactData.name;
5953
+ this.messengerState.userContext.email = contactData.email;
5954
+ }
5955
+
5956
+ return response;
5957
+ } catch (error) {
5958
+ console.error('[MessengerWidget] Failed to identify contact:', error);
5959
+ throw error;
5960
+ }
5961
+ }
5962
+
5963
+ async _handleUploadFile(base64Data, filename) {
5964
+ try {
5965
+ const response = await this.apiService.uploadFile(base64Data, filename);
5966
+ if (response.status && response.url) {
5967
+ return response.url;
5968
+ }
5969
+ throw new Error('Upload failed');
5970
+ } catch (error) {
5971
+ console.error('[MessengerWidget] Failed to upload file:', error);
5972
+ throw error;
5973
+ }
5974
+ }
5975
+
5976
+ async _uploadPendingAttachments(pendingAttachments) {
5977
+ if (!pendingAttachments || pendingAttachments.length === 0) return [];
5978
+
5979
+ const uploaded = [];
5980
+ for (const att of pendingAttachments) {
5981
+ try {
5982
+ const cdnUrl = await this._handleUploadFile(att.preview, att.file.name);
5983
+ uploaded.push({
5984
+ url: cdnUrl,
5985
+ type: att.type.startsWith('image') ? 'image' : 'file',
5986
+ name: att.file.name,
5987
+ });
5988
+ } catch (err) {
5989
+ console.error('[MessengerWidget] Skipping failed attachment upload:', att.file.name, err);
5990
+ }
5991
+ }
5992
+ return uploaded;
5067
5993
  }
5068
5994
 
5069
- async _handleSendMessage(conversationId, message) {
5995
+ async _handleSendMessage(conversationId, message, pendingAttachments) {
5070
5996
  // Emit event for external listeners
5071
5997
  this.sdk.eventBus.emit('messenger:messageSent', {
5072
5998
  widget: this,
@@ -5075,9 +6001,13 @@
5075
6001
  });
5076
6002
 
5077
6003
  try {
6004
+ // Upload attachments to CDN first
6005
+ const uploadedAttachments = await this._uploadPendingAttachments(pendingAttachments);
6006
+
5078
6007
  // Send message through API
5079
6008
  const response = await this.apiService.sendMessage(conversationId, {
5080
6009
  content: message.content,
6010
+ attachments: uploadedAttachments,
5081
6011
  });
5082
6012
 
5083
6013
  if (response.status && response.data) {
@@ -5114,12 +6044,25 @@
5114
6044
  _handleWebSocketMessage(data) {
5115
6045
  const { conversation_id, message } = data;
5116
6046
 
6047
+ // Parse attachments from server message
6048
+ let attachments = [];
6049
+ if (message.attachments) {
6050
+ try {
6051
+ attachments = typeof message.attachments === 'string'
6052
+ ? JSON.parse(message.attachments)
6053
+ : message.attachments;
6054
+ } catch (e) {
6055
+ // ignore parse errors
6056
+ }
6057
+ }
6058
+
5117
6059
  // Transform message to local format
5118
6060
  const localMessage = {
5119
6061
  id: message.id,
5120
6062
  content: message.content,
5121
6063
  isOwn: message.sender_type === 'customer',
5122
6064
  timestamp: message.created_at,
6065
+ attachments: attachments.length > 0 ? attachments : undefined,
5123
6066
  sender: {
5124
6067
  name: message.sender_name || 'Support',
5125
6068
  avatarUrl: message.sender_avatar || null,
@@ -5159,6 +6102,19 @@
5159
6102
  });
5160
6103
  }
5161
6104
 
6105
+ /**
6106
+ * Handle conversation closed event
6107
+ */
6108
+ _handleConversationClosed(data) {
6109
+ const conversationId =
6110
+ data?.conversation_id ||
6111
+ data?.id ||
6112
+ data?.conversation?.id;
6113
+ if (!conversationId) return;
6114
+
6115
+ this.messengerState.updateConversation(conversationId, { status: 'closed' });
6116
+ }
6117
+
5162
6118
  /**
5163
6119
  * Update unread count from API
5164
6120
  */
@@ -5201,9 +6157,18 @@
5201
6157
  this._wsUnsubscribers.push(
5202
6158
  this.wsService.on('typing_stopped', this._handleTypingStopped)
5203
6159
  );
6160
+ this._wsUnsubscribers.push(
6161
+ this.wsService.on('conversation_closed', this._handleConversationClosed)
6162
+ );
5204
6163
  this._wsUnsubscribers.push(
5205
6164
  this.wsService.on('connected', () => {
5206
6165
  console.log('[MessengerWidget] WebSocket connected');
6166
+ // Re-subscribe to active conversation on reconnect
6167
+ if (this.messengerState.activeConversationId) {
6168
+ this.wsService.send('conversation:subscribe', {
6169
+ conversation_id: this.messengerState.activeConversationId,
6170
+ });
6171
+ }
5207
6172
  })
5208
6173
  );
5209
6174
  this._wsUnsubscribers.push(
@@ -5396,18 +6361,29 @@
5396
6361
  try {
5397
6362
  const response = await this.apiService.getConversation(conversationId);
5398
6363
  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
- }));
6364
+ const messages = (response.data.messages || []).map((msg) => {
6365
+ let attachments;
6366
+ if (msg.attachments) {
6367
+ try {
6368
+ attachments = typeof msg.attachments === 'string'
6369
+ ? JSON.parse(msg.attachments)
6370
+ : msg.attachments;
6371
+ } catch (e) {
6372
+ // ignore parse errors
6373
+ }
6374
+ }
6375
+ return {
6376
+ id: msg.id,
6377
+ content: msg.content,
6378
+ isOwn: msg.sender_type === 'customer',
6379
+ timestamp: msg.created_at,
6380
+ attachments: attachments && attachments.length > 0 ? attachments : undefined,
6381
+ sender: {
6382
+ name: msg.sender_name || (msg.sender_type === 'customer' ? 'You' : 'Support'),
6383
+ avatarUrl: msg.sender_avatar || null,
6384
+ },
6385
+ };
6386
+ });
5411
6387
  this.messengerState.setMessages(conversationId, messages);
5412
6388
 
5413
6389
  // Mark as read
@@ -5426,13 +6402,28 @@
5426
6402
  /**
5427
6403
  * Start a new conversation
5428
6404
  */
5429
- async startNewConversation(message, subject = '') {
6405
+ async startNewConversation(message, subject = '', pendingAttachments = []) {
5430
6406
  try {
6407
+ // Upload attachments to CDN first
6408
+ const uploadedAttachments = await this._uploadPendingAttachments(pendingAttachments);
6409
+
6410
+ console.log('[MessengerWidget] Starting conversation...', {
6411
+ message,
6412
+ attachmentCount: uploadedAttachments.length,
6413
+ hasSession: this.apiService.isSessionValid(),
6414
+ sessionToken: this.apiService.sessionToken ? this.apiService.sessionToken.substring(0, 10) + '...' : null,
6415
+ baseURL: this.apiService.baseURL,
6416
+ mock: this.apiService.mock,
6417
+ });
6418
+
5431
6419
  const response = await this.apiService.startConversation({
5432
6420
  message,
5433
6421
  subject,
6422
+ attachments: uploadedAttachments,
5434
6423
  });
5435
6424
 
6425
+ console.log('[MessengerWidget] Conversation response:', response);
6426
+
5436
6427
  if (response.status && response.data) {
5437
6428
  const conv = response.data;
5438
6429
  const newConversation = {
@@ -5492,6 +6483,12 @@
5492
6483
  this.messengerState.agentsOnline = response.data.agents_online;
5493
6484
  this.messengerState.onlineCount = response.data.online_count || 0;
5494
6485
  this.messengerState.responseTime = response.data.response_time || '';
6486
+
6487
+ // Update team avatars from online agents
6488
+ if (response.data.available_agents) {
6489
+ this.messengerState.setTeamAvatarsFromAgents(response.data.available_agents);
6490
+ }
6491
+
5495
6492
  this.messengerState._notify('availabilityUpdate', response.data);
5496
6493
  return response.data;
5497
6494
  }
@@ -6404,8 +7401,11 @@
6404
7401
  this.apiService = new APIService({
6405
7402
  apiUrl: this.config.apiUrl,
6406
7403
  workspace: this.config.workspace,
7404
+ siteId: this.config.siteId,
7405
+ sessionToken: this.config.sessionToken,
6407
7406
  userContext: this.config.userContext,
6408
7407
  mock: this.config.mock,
7408
+ debug: this.config.debug,
6409
7409
  env: this.config.env,
6410
7410
  });
6411
7411
 
@@ -7201,6 +8201,39 @@
7201
8201
  opacity: 0.6;
7202
8202
  }
7203
8203
 
8204
+ /* Continue conversation variant */
8205
+ .messenger-home-continue-btn {
8206
+ flex-direction: column;
8207
+ align-items: flex-start;
8208
+ gap: 2px;
8209
+ position: relative;
8210
+ }
8211
+
8212
+ .messenger-home-continue-btn > i {
8213
+ position: absolute;
8214
+ right: 20px;
8215
+ top: 50%;
8216
+ transform: translateY(-50%);
8217
+ }
8218
+
8219
+ .messenger-home-continue-info {
8220
+ display: flex;
8221
+ flex-direction: column;
8222
+ gap: 2px;
8223
+ text-align: left;
8224
+ }
8225
+
8226
+ .messenger-home-continue-label {
8227
+ font-size: 14px;
8228
+ font-weight: 600;
8229
+ }
8230
+
8231
+ .messenger-home-continue-preview {
8232
+ font-size: 12px;
8233
+ opacity: 0.6;
8234
+ font-weight: 400;
8235
+ }
8236
+
7204
8237
  /* Featured Card */
7205
8238
  .messenger-home-featured {
7206
8239
  background: #2c2c2e;
@@ -7329,7 +8362,7 @@
7329
8362
  .messenger-conversations-body {
7330
8363
  flex: 1;
7331
8364
  overflow-y: auto;
7332
- padding: 12px;
8365
+ padding: 4px 12px 12px 12px;
7333
8366
  }
7334
8367
 
7335
8368
  .messenger-conversations-empty {
@@ -7364,7 +8397,7 @@
7364
8397
  display: flex;
7365
8398
  align-items: flex-start;
7366
8399
  gap: 12px;
7367
- padding: 16px;
8400
+ padding: 10px 16px;
7368
8401
  border-radius: 12px;
7369
8402
  cursor: pointer;
7370
8403
  transition: background 0.2s ease;
@@ -7496,6 +8529,11 @@
7496
8529
  color: white;
7497
8530
  }
7498
8531
 
8532
+ .messenger-chat-view {
8533
+ position: relative;
8534
+ overflow: visible;
8535
+ }
8536
+
7499
8537
  .messenger-chat-messages {
7500
8538
  flex: 1;
7501
8539
  overflow-y: auto;
@@ -7570,7 +8608,7 @@
7570
8608
  }
7571
8609
 
7572
8610
  .messenger-message-own .messenger-message-bubble {
7573
- background: #007aff;
8611
+ background: rgb(29, 78, 216);
7574
8612
  color: white;
7575
8613
  border-bottom-right-radius: 4px;
7576
8614
  }
@@ -7598,10 +8636,25 @@
7598
8636
  margin-top: auto;
7599
8637
  }
7600
8638
 
8639
+ /* Conversation Closed Banner */
8640
+ .messenger-closed-banner {
8641
+ display: flex;
8642
+ align-items: center;
8643
+ justify-content: center;
8644
+ gap: 8px;
8645
+ padding: 12px 16px;
8646
+ margin: 16px;
8647
+ background: rgba(52, 199, 89, 0.12);
8648
+ color: #34c759;
8649
+ border-radius: 12px;
8650
+ font-size: 13px;
8651
+ font-weight: 500;
8652
+ }
8653
+
7601
8654
  /* Compose Area */
7602
8655
  .messenger-chat-compose {
7603
8656
  display: flex;
7604
- align-items: flex-end;
8657
+ align-items: center;
7605
8658
  gap: 8px;
7606
8659
  padding: 12px 16px;
7607
8660
  border-top: 1px solid rgba(255, 255, 255, 0.1);
@@ -7611,7 +8664,7 @@
7611
8664
  .messenger-compose-input-wrapper {
7612
8665
  flex: 1;
7613
8666
  background: #2c2c2e;
7614
- border-radius: 20px;
8667
+ border-radius: 10px;
7615
8668
  padding: 8px 16px;
7616
8669
  }
7617
8670
 
@@ -7647,15 +8700,176 @@
7647
8700
  flex-shrink: 0;
7648
8701
  }
7649
8702
 
7650
- .messenger-compose-send:hover:not(:disabled) {
7651
- background: #0066d6;
7652
- transform: scale(1.05);
8703
+ .messenger-compose-send:hover:not(:disabled) {
8704
+ background: #0066d6;
8705
+ transform: scale(1.05);
8706
+ }
8707
+
8708
+ .messenger-compose-send:disabled {
8709
+ background: #3c3c3e;
8710
+ color: rgba(255, 255, 255, 0.3);
8711
+ cursor: not-allowed;
8712
+ }
8713
+
8714
+ /* Attach Button */
8715
+ .messenger-compose-attach {
8716
+ width: 40px;
8717
+ height: 40px;
8718
+ background: transparent;
8719
+ border: none;
8720
+ border-radius: 50%;
8721
+ color: rgba(255, 255, 255, 0.5);
8722
+ cursor: pointer;
8723
+ display: flex;
8724
+ align-items: center;
8725
+ justify-content: center;
8726
+ transition: all 0.2s ease;
8727
+ flex-shrink: 0;
8728
+ }
8729
+
8730
+ .messenger-compose-attach:hover:not(:disabled) {
8731
+ color: rgba(255, 255, 255, 0.85);
8732
+ background: rgba(255, 255, 255, 0.08);
8733
+ }
8734
+
8735
+ .messenger-compose-attach:disabled {
8736
+ opacity: 0.3;
8737
+ cursor: not-allowed;
8738
+ }
8739
+
8740
+ /* Attachment Preview Strip */
8741
+ .messenger-compose-attachments-preview {
8742
+ display: none;
8743
+ flex-wrap: wrap;
8744
+ gap: 8px;
8745
+ padding: 8px 16px;
8746
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
8747
+ background: #1c1c1e;
8748
+ }
8749
+
8750
+ .messenger-attachment-preview {
8751
+ position: relative;
8752
+ width: 56px;
8753
+ height: 56px;
8754
+ border-radius: 8px;
8755
+ overflow: hidden;
8756
+ border: 1px solid rgba(255, 255, 255, 0.15);
8757
+ }
8758
+
8759
+ .messenger-attachment-thumb {
8760
+ width: 100%;
8761
+ height: 100%;
8762
+ object-fit: cover;
8763
+ display: block;
8764
+ }
8765
+
8766
+ .messenger-attachment-file-icon {
8767
+ display: flex;
8768
+ align-items: center;
8769
+ justify-content: center;
8770
+ background: #2c2c2e;
8771
+ color: rgba(255, 255, 255, 0.5);
8772
+ }
8773
+
8774
+ .messenger-attachment-remove {
8775
+ position: absolute;
8776
+ top: 2px;
8777
+ right: 2px;
8778
+ width: 18px;
8779
+ height: 18px;
8780
+ background: rgba(0, 0, 0, 0.7);
8781
+ border: none;
8782
+ border-radius: 50%;
8783
+ color: white;
8784
+ font-size: 12px;
8785
+ line-height: 1;
8786
+ cursor: pointer;
8787
+ display: flex;
8788
+ align-items: center;
8789
+ justify-content: center;
8790
+ padding: 0;
8791
+ transition: background 0.15s ease;
8792
+ }
8793
+
8794
+ .messenger-attachment-remove:hover {
8795
+ background: rgba(255, 59, 48, 0.85);
8796
+ }
8797
+
8798
+ /* Message Attachments (inline images & file links) */
8799
+ .messenger-message-image {
8800
+ max-width: 220px;
8801
+ max-height: 200px;
8802
+ width: auto;
8803
+ height: auto;
8804
+ border-radius: 8px;
8805
+ margin-top: 4px;
8806
+ cursor: pointer;
8807
+ object-fit: contain;
8808
+ display: block;
8809
+ }
8810
+
8811
+ .messenger-message-file {
8812
+ display: inline-flex;
8813
+ align-items: center;
8814
+ gap: 6px;
8815
+ margin-top: 4px;
8816
+ padding: 8px 12px;
8817
+ border-radius: 8px;
8818
+ background: #2c2c2e;
8819
+ color: #60a5fa;
8820
+ text-decoration: none;
8821
+ font-size: 13px;
8822
+ transition: background 0.15s ease;
8823
+ max-width: 100%;
8824
+ word-break: break-all;
8825
+ cursor: pointer;
8826
+ }
8827
+
8828
+ .messenger-message-file:hover {
8829
+ background: #3c3c3e;
8830
+ }
8831
+
8832
+ .messenger-file-download-icon {
8833
+ margin-left: auto;
8834
+ opacity: 0.5;
8835
+ flex-shrink: 0;
8836
+ }
8837
+
8838
+ .messenger-message-file:hover .messenger-file-download-icon {
8839
+ opacity: 1;
8840
+ }
8841
+
8842
+ /* Light theme overrides for attachments */
8843
+ .theme-light .messenger-compose-attach {
8844
+ color: #9ca3af;
8845
+ }
8846
+
8847
+ .theme-light .messenger-compose-attach:hover:not(:disabled) {
8848
+ color: #374151;
8849
+ background: #f3f4f6;
8850
+ }
8851
+
8852
+ .theme-light .messenger-compose-attachments-preview {
8853
+ background: #ffffff;
8854
+ border-top-color: #e5e7eb;
8855
+ }
8856
+
8857
+ .theme-light .messenger-attachment-preview {
8858
+ border-color: #e5e7eb;
8859
+ }
8860
+
8861
+ .theme-light .messenger-attachment-file-icon {
8862
+ background: #f3f4f6;
8863
+ color: #6b7280;
8864
+ }
8865
+
8866
+ .theme-light .messenger-message-file {
8867
+ background: #f3f4f6;
8868
+ color: #2563eb;
7653
8869
  }
7654
8870
 
7655
- .messenger-compose-send:disabled {
7656
- background: #3c3c3e;
7657
- color: rgba(255, 255, 255, 0.3);
7658
- cursor: not-allowed;
8871
+ .theme-light .messenger-message-file:hover {
8872
+ background: #e5e7eb;
7659
8873
  }
7660
8874
 
7661
8875
  /* ========================================
@@ -7972,7 +9186,7 @@
7972
9186
 
7973
9187
  .messenger-nav {
7974
9188
  display: flex;
7975
- padding: 8px 12px;
9189
+ padding: 4px 8px;
7976
9190
  gap: 4px;
7977
9191
  }
7978
9192
 
@@ -8025,8 +9239,8 @@
8025
9239
  }
8026
9240
 
8027
9241
  .messenger-nav-label {
8028
- font-size: 14px;
8029
- font-weight: 400;
9242
+ font-size: 11px;
9243
+ font-weight: 500;
8030
9244
  color: rgba(255, 255, 255, 0.5);
8031
9245
  transition: color 0.2s ease;
8032
9246
  }
@@ -8285,7 +9499,7 @@
8285
9499
  }
8286
9500
 
8287
9501
  .theme-light .messenger-message-own .messenger-message-bubble {
8288
- background: #007aff;
9502
+ background: rgb(29, 78, 216);
8289
9503
  color: #ffffff;
8290
9504
  }
8291
9505
 
@@ -8293,6 +9507,11 @@
8293
9507
  color: #86868b;
8294
9508
  }
8295
9509
 
9510
+ .theme-light .messenger-closed-banner {
9511
+ background: rgba(52, 199, 89, 0.1);
9512
+ color: #22883a;
9513
+ }
9514
+
8296
9515
  .theme-light .messenger-chat-compose {
8297
9516
  background: #ffffff;
8298
9517
  border-top-color: #e5e5e7;
@@ -8559,6 +9778,191 @@
8559
9778
  }
8560
9779
  }
8561
9780
 
9781
+ /* ========================================
9782
+ Pre-Chat Form View (Transparent Overlay)
9783
+ ======================================== */
9784
+
9785
+ .messenger-prechat-view {
9786
+ background: transparent;
9787
+ position: relative;
9788
+ }
9789
+
9790
+ .messenger-prechat-overlay {
9791
+ position: absolute;
9792
+ top: 0;
9793
+ left: 0;
9794
+ right: 0;
9795
+ bottom: 0;
9796
+ background: rgba(0, 0, 0, 0.5);
9797
+ backdrop-filter: blur(2px);
9798
+ display: flex;
9799
+ align-items: flex-end;
9800
+ padding: 16px;
9801
+ animation: messenger-fade-in 0.2s ease;
9802
+ }
9803
+
9804
+ .messenger-prechat-card {
9805
+ background: #1c1c1e;
9806
+ border-radius: 16px;
9807
+ padding: 20px;
9808
+ width: 100%;
9809
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
9810
+ animation: messenger-slide-up 0.25s ease;
9811
+ }
9812
+
9813
+ .messenger-prechat-card h4 {
9814
+ margin: 0 0 14px;
9815
+ font-size: 15px;
9816
+ font-weight: 600;
9817
+ color: white;
9818
+ text-align: center;
9819
+ }
9820
+
9821
+ .messenger-prechat-form {
9822
+ display: flex;
9823
+ flex-direction: column;
9824
+ gap: 10px;
9825
+ }
9826
+
9827
+ .messenger-prechat-fields {
9828
+ display: flex;
9829
+ flex-direction: column;
9830
+ gap: 8px;
9831
+ }
9832
+
9833
+ .messenger-prechat-input {
9834
+ width: 100%;
9835
+ padding: 11px 14px;
9836
+ background: #2c2c2e;
9837
+ border: 1px solid rgba(255, 255, 255, 0.1);
9838
+ border-radius: 10px;
9839
+ color: white;
9840
+ font-size: 14px;
9841
+ font-family: inherit;
9842
+ outline: none;
9843
+ transition: border-color 0.2s ease;
9844
+ }
9845
+
9846
+ .messenger-prechat-input:focus {
9847
+ border-color: #007aff;
9848
+ }
9849
+
9850
+ .messenger-prechat-input::placeholder {
9851
+ color: rgba(255, 255, 255, 0.4);
9852
+ }
9853
+
9854
+ .messenger-prechat-error {
9855
+ font-size: 12px;
9856
+ color: #ef4444;
9857
+ display: none;
9858
+ text-align: center;
9859
+ }
9860
+
9861
+ .messenger-prechat-actions {
9862
+ display: flex;
9863
+ gap: 10px;
9864
+ margin-top: 4px;
9865
+ }
9866
+
9867
+ .messenger-prechat-skip {
9868
+ flex: 1;
9869
+ padding: 11px 14px;
9870
+ background: transparent;
9871
+ border: 1px solid rgba(255, 255, 255, 0.2);
9872
+ border-radius: 20px;
9873
+ color: rgba(255, 255, 255, 0.7);
9874
+ font-size: 14px;
9875
+ font-weight: 500;
9876
+ cursor: pointer;
9877
+ transition: all 0.2s ease;
9878
+ }
9879
+
9880
+ .messenger-prechat-skip:hover {
9881
+ background: rgba(255, 255, 255, 0.05);
9882
+ border-color: rgba(255, 255, 255, 0.3);
9883
+ }
9884
+
9885
+ .messenger-prechat-submit {
9886
+ flex: 1;
9887
+ display: flex;
9888
+ align-items: center;
9889
+ justify-content: center;
9890
+ gap: 6px;
9891
+ padding: 11px 14px;
9892
+ background: #007aff;
9893
+ border: none;
9894
+ border-radius: 20px;
9895
+ color: white;
9896
+ font-size: 14px;
9897
+ font-weight: 600;
9898
+ cursor: pointer;
9899
+ transition: all 0.2s ease;
9900
+ }
9901
+
9902
+ .messenger-prechat-submit:hover:not(:disabled) {
9903
+ background: #0066d6;
9904
+ }
9905
+
9906
+ .messenger-prechat-submit:disabled {
9907
+ background: #3c3c3e;
9908
+ color: rgba(255, 255, 255, 0.4);
9909
+ cursor: not-allowed;
9910
+ }
9911
+
9912
+ .messenger-prechat-submit-loading {
9913
+ display: inline-flex;
9914
+ align-items: center;
9915
+ animation: messenger-spin 1s linear infinite;
9916
+ }
9917
+
9918
+ @keyframes messenger-spin {
9919
+ from { transform: rotate(0deg); }
9920
+ to { transform: rotate(360deg); }
9921
+ }
9922
+
9923
+ /* Light Theme - Pre-Chat Form */
9924
+ .theme-light .messenger-prechat-overlay {
9925
+ background: rgba(255, 255, 255, 0.6);
9926
+ }
9927
+
9928
+ .theme-light .messenger-prechat-card {
9929
+ background: #ffffff;
9930
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
9931
+ }
9932
+
9933
+ .theme-light .messenger-prechat-card h4 {
9934
+ color: #1d1d1f;
9935
+ }
9936
+
9937
+ .theme-light .messenger-prechat-input {
9938
+ background: #f5f5f7;
9939
+ border-color: #e5e5e7;
9940
+ color: #1d1d1f;
9941
+ }
9942
+
9943
+ .theme-light .messenger-prechat-input:focus {
9944
+ border-color: #007aff;
9945
+ background: #ffffff;
9946
+ }
9947
+
9948
+ .theme-light .messenger-prechat-input::placeholder {
9949
+ color: #86868b;
9950
+ }
9951
+
9952
+ .theme-light .messenger-prechat-skip {
9953
+ border-color: #e5e5e7;
9954
+ color: #6e6e73;
9955
+ }
9956
+
9957
+ .theme-light .messenger-prechat-skip:hover {
9958
+ background: #f5f5f7;
9959
+ }
9960
+
9961
+ .theme-light .messenger-prechat-submit:disabled {
9962
+ background: #e5e5e7;
9963
+ color: #c7c7cc;
9964
+ }
9965
+
8562
9966
  /* ========================================
8563
9967
  Animations
8564
9968
  ======================================== */
@@ -8590,6 +9994,173 @@
8590
9994
  transition: none;
8591
9995
  }
8592
9996
  }
9997
+
9998
+ /* ========================================
9999
+ Email Collection Overlay (Bottom Sheet)
10000
+ ======================================== */
10001
+
10002
+ .messenger-email-overlay {
10003
+ position: absolute;
10004
+ bottom: -56px;
10005
+ left: 0;
10006
+ right: 0;
10007
+ top: 0;
10008
+ display: flex;
10009
+ align-items: flex-end;
10010
+ z-index: 20;
10011
+ background: rgba(0, 0, 0, 0.08);
10012
+ pointer-events: auto;
10013
+ }
10014
+
10015
+ .messenger-email-card {
10016
+ width: 100%;
10017
+ background: #ffffff;
10018
+ border-radius: 0;
10019
+ padding: 16px 16px 72px;
10020
+ box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.08);
10021
+ animation: messenger-slide-up 0.25s ease;
10022
+ }
10023
+
10024
+ .messenger-email-card h4 {
10025
+ margin: 0 0 2px;
10026
+ font-size: 13px;
10027
+ font-weight: 600;
10028
+ color: #1d1d1f;
10029
+ text-align: center;
10030
+ }
10031
+
10032
+ .messenger-email-card p {
10033
+ margin: 0 0 10px;
10034
+ font-size: 11px;
10035
+ color: #6b7280;
10036
+ text-align: center;
10037
+ }
10038
+
10039
+ .messenger-email-name,
10040
+ .messenger-email-input {
10041
+ width: 100%;
10042
+ padding: 8px 10px;
10043
+ background: #f3f4f6;
10044
+ border: 1px solid transparent;
10045
+ border-radius: 8px;
10046
+ color: #1d1d1f;
10047
+ font-size: 12px;
10048
+ font-family: inherit;
10049
+ outline: none;
10050
+ margin-bottom: 6px;
10051
+ transition: border-color 0.2s ease, box-shadow 0.2s ease;
10052
+ }
10053
+
10054
+ .messenger-email-name:focus,
10055
+ .messenger-email-input:focus {
10056
+ border-color: #007aff;
10057
+ box-shadow: 0 0 0 2px rgba(0, 122, 255, 0.12);
10058
+ background: #ffffff;
10059
+ }
10060
+
10061
+ .messenger-email-name::placeholder,
10062
+ .messenger-email-input::placeholder {
10063
+ color: #9ca3af;
10064
+ }
10065
+
10066
+ .messenger-email-actions {
10067
+ display: flex;
10068
+ gap: 8px;
10069
+ margin-top: 4px;
10070
+ }
10071
+
10072
+ .messenger-email-submit {
10073
+ flex: 1.2;
10074
+ padding: 7px 12px;
10075
+ background: #007aff;
10076
+ border: none;
10077
+ border-radius: 8px;
10078
+ color: white;
10079
+ font-size: 12px;
10080
+ font-weight: 600;
10081
+ cursor: pointer;
10082
+ transition: all 0.2s ease;
10083
+ }
10084
+
10085
+ .messenger-email-submit:hover:not(:disabled) {
10086
+ background: #0066d6;
10087
+ }
10088
+
10089
+ .messenger-email-submit:disabled {
10090
+ background: #d1d5db;
10091
+ color: #9ca3af;
10092
+ cursor: not-allowed;
10093
+ }
10094
+
10095
+ .messenger-email-skip {
10096
+ flex: 0.8;
10097
+ padding: 7px 12px;
10098
+ background: #ffffff;
10099
+ border: 1px solid #e5e5e7;
10100
+ border-radius: 8px;
10101
+ color: #4b5563;
10102
+ font-size: 12px;
10103
+ font-weight: 500;
10104
+ cursor: pointer;
10105
+ transition: all 0.2s ease;
10106
+ }
10107
+
10108
+ .messenger-email-skip:hover {
10109
+ background: #f9fafb;
10110
+ border-color: #d1d5db;
10111
+ }
10112
+
10113
+ /* Dark Theme - Email Overlay */
10114
+ .theme-dark .messenger-email-overlay {
10115
+ background: rgba(0, 0, 0, 0.3);
10116
+ }
10117
+
10118
+ .theme-dark .messenger-email-card {
10119
+ background: #1c1c1e;
10120
+ }
10121
+
10122
+ .theme-dark .messenger-email-card h4 {
10123
+ color: white;
10124
+ }
10125
+
10126
+ .theme-dark .messenger-email-card p {
10127
+ color: rgba(255, 255, 255, 0.6);
10128
+ }
10129
+
10130
+ .theme-dark .messenger-email-name,
10131
+ .theme-dark .messenger-email-input {
10132
+ background: #2c2c2e;
10133
+ border-color: transparent;
10134
+ color: white;
10135
+ }
10136
+
10137
+ .theme-dark .messenger-email-name:focus,
10138
+ .theme-dark .messenger-email-input:focus {
10139
+ border-color: #007aff;
10140
+ box-shadow: 0 0 0 2px rgba(0, 122, 255, 0.25);
10141
+ background: #1c1c1e;
10142
+ }
10143
+
10144
+ .theme-dark .messenger-email-name::placeholder,
10145
+ .theme-dark .messenger-email-input::placeholder {
10146
+ color: rgba(255, 255, 255, 0.4);
10147
+ }
10148
+
10149
+ .theme-dark .messenger-email-submit:disabled {
10150
+ background: #3c3c3e;
10151
+ color: rgba(255, 255, 255, 0.4);
10152
+ }
10153
+
10154
+ .theme-dark .messenger-email-skip {
10155
+ background: transparent;
10156
+ border-color: rgba(255, 255, 255, 0.15);
10157
+ color: rgba(255, 255, 255, 0.8);
10158
+ }
10159
+
10160
+ .theme-dark .messenger-email-skip:hover {
10161
+ background: rgba(255, 255, 255, 0.05);
10162
+ border-color: rgba(255, 255, 255, 0.25);
10163
+ }
8593
10164
  `;
8594
10165
 
8595
10166
  const baseStyles = `
@@ -10029,7 +11600,7 @@
10029
11600
  EventBus,
10030
11601
  APIService,
10031
11602
  SDKError,
10032
- APIError,
11603
+ APIError: APIError$1,
10033
11604
  WidgetError,
10034
11605
  ConfigError,
10035
11606
  ValidationError,
@@ -10099,7 +11670,7 @@
10099
11670
  handleDOMReady();
10100
11671
  }
10101
11672
 
10102
- exports.APIError = APIError;
11673
+ exports.APIError = APIError$1;
10103
11674
  exports.APIService = APIService;
10104
11675
  exports.BaseWidget = BaseWidget;
10105
11676
  exports.ButtonWidget = ButtonWidget;