@ihoomanai/chat-widget 3.0.0 → 3.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.umd.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @ihoomanai/chat-widget v3.0.0
2
+ * @ihoomanai/chat-widget v3.0.2
3
3
  * Universal chat support widget for any website - secure Widget ID based initialization
4
4
  *
5
5
  * @license MIT
@@ -13,27 +13,14 @@
13
13
 
14
14
  /**
15
15
  * Ihooman Chat Widget - Core Implementation
16
- *
17
- * Secure Widget ID based initialization - no API keys in client code.
18
- * Fetches configuration from Widget Configuration API using Widget ID.
19
- *
20
- * @module widget
21
- * @version 2.0.0
22
- * @license MIT
23
- *
24
- * Requirements:
25
- * - 5.2: Export IhoomanChat object with init, open, close, toggle, destroy methods
26
- * - 5.3: Accept widgetId configuration option for initialization
27
- * - 10.1: Widget only requires Widget ID, never API key
28
- * - 10.2: Widget fetches configuration using only Widget ID
16
+ * Enhanced with professional features
17
+ * @version 3.0.0
29
18
  */
30
- const VERSION = '2.0.0';
19
+ const VERSION = '3.0.2';
31
20
  const STORAGE_PREFIX = 'ihooman_chat_';
32
- /**
33
- * Default widget configuration
34
- */
21
+ const DEFAULT_SERVER_URL = 'https://api.ihooman.ai';
35
22
  const defaultConfig = {
36
- serverUrl: 'https://api.ihooman.ai',
23
+ serverUrl: DEFAULT_SERVER_URL,
37
24
  theme: 'light',
38
25
  position: 'bottom-right',
39
26
  title: 'Chat Support',
@@ -57,10 +44,12 @@
57
44
  fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
58
45
  avatarUrl: '',
59
46
  poweredBy: true,
47
+ presetQuestions: [],
48
+ proactiveMessages: [],
49
+ surveyConfig: null,
50
+ locale: 'en',
51
+ allowLocalhost: true,
60
52
  };
61
- /**
62
- * SVG icons for the widget UI
63
- */
64
53
  const icons = {
65
54
  chat: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg>`,
66
55
  close: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>`,
@@ -69,60 +58,65 @@
69
58
  minimize: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="5" y1="12" x2="19" y2="12"></line></svg>`,
70
59
  bot: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="10" rx="2"></rect><circle cx="12" cy="5" r="2"></circle><path d="M12 7v4"></path></svg>`,
71
60
  agent: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle></svg>`,
72
- ticket: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><line x1="10" y1="9" x2="8" y2="9"></line></svg>`,
61
+ ticket: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"></path><polyline points="14 2 14 8 20 8"></polyline></svg>`,
73
62
  history: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>`,
74
63
  plus: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>`,
75
- };
76
- /**
77
- * Internal widget state
78
- */
64
+ star: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"></path></svg>`,
65
+ starEmpty: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"></path></svg>`,
66
+ thumbUp: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"></path></svg>`,
67
+ thumbDown: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17"></path></svg>`};
79
68
  let config = { widgetId: '', ...defaultConfig };
80
69
  let state = {
81
70
  isOpen: false,
82
71
  isConnected: false,
83
72
  messages: [],
73
+ pendingMessages: [],
84
74
  sessionId: null,
85
75
  visitorId: null,
86
76
  unreadCount: 0,
77
+ view: 'chat',
78
+ connectionStatus: 'disconnected',
79
+ typingIndicator: false,
80
+ soundMuted: false,
81
+ escalationStatus: null,
82
+ userInfo: null,
87
83
  };
88
84
  let currentView = 'chat';
89
85
  let isLiveAgentMode = false;
86
+ let shownProactiveIds = [];
87
+ let proactiveCooldowns = {};
88
+ let proactiveCheckInterval = null;
89
+ let presetQuestions = [];
90
90
  let elements = {};
91
91
  const eventListeners = {};
92
- /**
93
- * WebSocket and polling state
94
- */
95
92
  let ws = null;
96
93
  let pollInterval = null;
97
94
  let reconnectAttempts = 0;
98
95
  const maxReconnectAttempts = 5;
99
- let intentionalDisconnect = false; // Flag to prevent reconnection on intentional close
100
- // ============================================================================
101
- // UTILITIES
102
- // ============================================================================
103
- /**
104
- * Generate a unique ID with optional prefix
105
- */
96
+ let intentionalDisconnect = false;
97
+ let heartbeatInterval = null;
98
+ let liveAgentPollInterval = null;
106
99
  function generateId(prefix = '') {
107
100
  return prefix + Math.random().toString(36).slice(2, 14) + Date.now().toString(36);
108
101
  }
109
- /**
110
- * Format a date to time string
111
- */
112
102
  function formatTime(date) {
113
103
  return new Date(date).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
114
104
  }
115
- /**
116
- * Escape HTML to prevent XSS
117
- */
105
+ function timeAgo(date) {
106
+ const diff = Date.now() - new Date(date).getTime();
107
+ if (diff < 60000)
108
+ return 'now';
109
+ if (diff < 3600000)
110
+ return Math.floor(diff / 60000) + 'm';
111
+ if (diff < 86400000)
112
+ return Math.floor(diff / 3600000) + 'h';
113
+ return Math.floor(diff / 86400000) + 'd';
114
+ }
118
115
  function escapeHtml(text) {
119
116
  const div = document.createElement('div');
120
117
  div.textContent = text;
121
118
  return div.innerHTML;
122
119
  }
123
- /**
124
- * Parse basic markdown to HTML
125
- */
126
120
  function parseMarkdown(text) {
127
121
  if (!text)
128
122
  return '';
@@ -130,11 +124,9 @@
130
124
  .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
131
125
  .replace(/\*(.*?)\*/g, '<em>$1</em>')
132
126
  .replace(/`(.*?)`/g, '<code>$1</code>')
127
+ .replace(/\[(.*?)\]\((.*?)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>')
133
128
  .replace(/\n/g, '<br>');
134
129
  }
135
- /**
136
- * Local storage helper with prefix
137
- */
138
130
  function storage(key, value) {
139
131
  const fullKey = STORAGE_PREFIX + key;
140
132
  try {
@@ -155,9 +147,6 @@
155
147
  return null;
156
148
  }
157
149
  }
158
- /**
159
- * Emit an event to all registered listeners
160
- */
161
150
  function emit(event, data) {
162
151
  const listeners = eventListeners[event] || [];
163
152
  listeners.forEach((fn) => {
@@ -165,10 +154,9 @@
165
154
  fn(data);
166
155
  }
167
156
  catch (e) {
168
- console.error(`Error in ${event} event handler:`, e);
157
+ console.error(`Error in ${event} handler:`, e);
169
158
  }
170
159
  });
171
- // Also call config callbacks
172
160
  const callbackName = `on${event.charAt(0).toUpperCase()}${event.slice(1)}`;
173
161
  const callback = config[callbackName];
174
162
  if (typeof callback === 'function') {
@@ -176,16 +164,27 @@
176
164
  callback(data);
177
165
  }
178
166
  catch (e) {
179
- console.error(`Error in ${callbackName} callback:`, e);
167
+ console.error(`Error in ${callbackName}:`, e);
180
168
  }
181
169
  }
182
170
  }
171
+ function getCurrentScrollDepth() {
172
+ const scrollTop = window.scrollY || document.documentElement.scrollTop;
173
+ const scrollHeight = document.documentElement.scrollHeight - window.innerHeight;
174
+ return scrollHeight > 0 ? Math.round((scrollTop / scrollHeight) * 100) : 0;
175
+ }
176
+ function matchUrlPattern(pattern) {
177
+ try {
178
+ const regex = new RegExp(pattern.replace(/\*/g, '.*'));
179
+ return regex.test(window.location.href);
180
+ }
181
+ catch {
182
+ return window.location.href.includes(pattern);
183
+ }
184
+ }
183
185
  // ============================================================================
184
186
  // STYLES
185
187
  // ============================================================================
186
- /**
187
- * Generate CSS styles for the widget
188
- */
189
188
  function generateStyles() {
190
189
  const { primaryColor, gradientFrom, gradientTo, fontFamily, borderRadius, zIndex, width, height, buttonSize } = config;
191
190
  const isDark = config.theme === 'dark';
@@ -201,7 +200,7 @@
201
200
  return `
202
201
  .ihooman-widget * { box-sizing: border-box; margin: 0; padding: 0; }
203
202
  .ihooman-widget { font-family: ${fontFamily}; font-size: 14px; line-height: 1.5; color: ${textColor}; -webkit-font-smoothing: antialiased; }
204
- .ihooman-widget button { border-radius: 8px; }
203
+ .ihooman-widget button { all: unset; box-sizing: border-box; cursor: pointer; font-family: inherit; }
205
204
  .ihooman-toggle { position: fixed; ${positionRight ? 'right: 20px' : 'left: 20px'}; ${positionBottom ? 'bottom: 20px' : 'top: 20px'}; width: ${buttonSize}px; height: ${buttonSize}px; border-radius: 50% !important; background: linear-gradient(135deg, ${gradientFrom}, ${gradientTo}); border: none; cursor: pointer; z-index: ${zIndex}; display: flex; align-items: center; justify-content: center; box-shadow: 0 4px 20px rgba(0, 174, 255, 0.35); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); overflow: hidden; }
206
205
  .ihooman-toggle:hover { transform: scale(1.08); box-shadow: 0 6px 28px rgba(0, 174, 255, 0.45); }
207
206
  .ihooman-toggle:active { transform: scale(0.95); }
@@ -231,9 +230,9 @@
231
230
  .ihooman-status-dot { width: 8px; height: 8px; border-radius: 50%; background: #22c55e; }
232
231
  .ihooman-status-dot.offline { background: #f59e0b; }
233
232
  .ihooman-header-actions { display: flex; gap: 8px; }
234
- .ihooman-header-btn { width: 32px; height: 32px; border-radius: 8px !important; background: rgba(255,255,255,0.15); border: none; cursor: pointer; display: flex; align-items: center; justify-content: center; color: white; transition: background 0.2s; }
233
+ .ihooman-header-btn { width: 28px; height: 28px; border-radius: 6px; background: rgba(255,255,255,0.15); border: none; cursor: pointer; display: flex; align-items: center; justify-content: center; color: white; transition: background 0.2s; }
235
234
  .ihooman-header-btn:hover { background: rgba(255,255,255,0.25); }
236
- .ihooman-header-btn svg { width: 16px; height: 16px; }
235
+ .ihooman-header-btn svg { width: 14px; height: 14px; }
237
236
  .ihooman-messages { flex: 1; overflow-y: auto; overflow-x: hidden; padding: 16px; display: flex; flex-direction: column; gap: 12px; background: ${bgColor}; overscroll-behavior: contain; min-height: 0; }
238
237
  .ihooman-messages::-webkit-scrollbar { width: 6px; }
239
238
  .ihooman-messages::-webkit-scrollbar-track { background: transparent; }
@@ -258,24 +257,22 @@
258
257
  .ihooman-input-wrapper:focus-within { border-color: ${primaryColor}; box-shadow: 0 0 0 3px rgba(0, 174, 255, 0.1); }
259
258
  .ihooman-input { flex: 1; border: none; background: transparent; font-family: inherit; font-size: 14px; color: ${textColor}; resize: none; max-height: 100px; outline: none; line-height: 1.4; }
260
259
  .ihooman-input::placeholder { color: ${mutedColor}; }
261
- .ihooman-input-btn { width: 36px; height: 36px; border-radius: 10px; border: none; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.2s; background: transparent; color: ${mutedColor}; }
260
+ .ihooman-input-btn { width: 32px; height: 32px; border-radius: 8px; border: none; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.2s; background: transparent; color: ${mutedColor}; }
262
261
  .ihooman-input-btn:hover { background: ${borderColor}; color: ${textColor}; }
263
262
  .ihooman-input-btn.send { background: linear-gradient(135deg, ${gradientFrom}, ${gradientTo}); color: white; }
264
- .ihooman-input-btn.send:hover { transform: scale(1.05); box-shadow: 0 4px 12px rgba(0, 174, 255, 0.3); }
265
- .ihooman-input-btn.send:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
266
- .ihooman-input-btn svg { width: 18px; height: 18px; }
263
+ .ihooman-input-btn.send:hover { opacity: 0.9; }
264
+ .ihooman-input-btn.send:disabled { opacity: 0.5; cursor: not-allowed; }
265
+ .ihooman-input-btn svg { width: 16px; height: 16px; }
267
266
  .ihooman-file-input { display: none; }
268
267
  .ihooman-powered { text-align: center; padding: 8px; font-size: 11px; color: ${mutedColor}; background: ${bgColor}; }
269
268
  .ihooman-powered a { color: ${primaryColor}; text-decoration: none; }
270
- .ihooman-error { padding: 16px; text-align: center; color: #ef4444; background: ${bgColor}; }
271
- .ihooman-escalation-actions { display: flex; gap: 10px; margin-top: 12px; flex-wrap: wrap; }
272
- .ihooman-widget .ihooman-escalation-btn { display: inline-flex !important; align-items: center !important; gap: 8px !important; padding: 10px 18px !important; border-radius: 8px !important; border: none !important; cursor: pointer !important; font-family: inherit !important; font-size: 13px !important; font-weight: 500 !important; transition: all 0.2s ease !important; width: auto !important; height: auto !important; min-width: 120px !important; }
273
- .ihooman-widget .ihooman-escalation-btn svg { width: 16px !important; height: 16px !important; flex-shrink: 0 !important; }
274
- .ihooman-widget .ihooman-escalation-btn.primary { background: linear-gradient(135deg, ${gradientFrom}, ${gradientTo}) !important; color: white !important; }
275
- .ihooman-widget .ihooman-escalation-btn.primary:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(0, 174, 255, 0.3); }
276
- .ihooman-widget .ihooman-escalation-btn.secondary { background: ${isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.05)'} !important; color: ${textColor} !important; border: 1px solid ${borderColor} !important; }
277
- .ihooman-widget .ihooman-escalation-btn.secondary:hover { background: ${isDark ? 'rgba(255,255,255,0.15)' : 'rgba(0,0,0,0.08)'} !important; }
278
- .ihooman-widget .ihooman-escalation-btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
269
+ .ihooman-escalation-actions { display: flex; gap: 8px; margin-top: 10px; flex-wrap: wrap; }
270
+ .ihooman-escalation-btn { display: inline-flex; align-items: center; justify-content: center; gap: 6px; padding: 8px 14px; border-radius: 6px; border: none; cursor: pointer; font-family: inherit; font-size: 12px; font-weight: 500; transition: all 0.2s ease; line-height: 1.4; }
271
+ .ihooman-escalation-btn svg { width: 14px; height: 14px; flex-shrink: 0; }
272
+ .ihooman-escalation-btn.primary { background: linear-gradient(135deg, ${gradientFrom}, ${gradientTo}); color: white; }
273
+ .ihooman-escalation-btn.primary:hover { opacity: 0.9; }
274
+ .ihooman-escalation-btn.secondary { background: ${isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.05)'}; color: ${textColor}; border: 1px solid ${borderColor}; }
275
+ .ihooman-escalation-btn.secondary:hover { background: ${isDark ? 'rgba(255,255,255,0.15)' : 'rgba(0,0,0,0.08)'}; }
279
276
  .ihooman-status-bar { padding: 10px 16px; text-align: center; font-size: 13px; display: none; }
280
277
  .ihooman-status-bar.show { display: block; }
281
278
  .ihooman-status-bar.waiting { background: #fef3c7; color: #92400e; }
@@ -284,24 +281,24 @@
284
281
  .ihooman-chat-view.hidden { display: none; }
285
282
  .ihooman-ticket-view { display: none; flex-direction: column; padding: 20px; gap: 16px; background: ${bgColor}; flex: 1; overflow-y: auto; min-height: 0; }
286
283
  .ihooman-ticket-view.show { display: flex; }
287
- .ihooman-ticket-title { font-size: 18px; font-weight: 600; color: ${textColor}; margin: 0; display: flex; align-items: center; gap: 8px; }
284
+ .ihooman-ticket-title { font-size: 18px; font-weight: 600; color: ${textColor}; margin: 0; }
288
285
  .ihooman-ticket-subtitle { font-size: 13px; color: ${mutedColor}; margin: 0; }
289
286
  .ihooman-ticket-input { padding: 12px 14px; border: 1px solid ${borderColor}; border-radius: 10px; font-size: 14px; font-family: inherit; background: ${inputBg}; color: ${textColor}; outline: none; transition: border-color 0.2s; }
290
287
  .ihooman-ticket-input:focus { border-color: ${primaryColor}; }
291
288
  .ihooman-ticket-input::placeholder { color: ${mutedColor}; }
292
289
  .ihooman-ticket-textarea { min-height: 100px; resize: vertical; }
293
- .ihooman-ticket-submit { padding: 14px; background: linear-gradient(135deg, ${gradientFrom}, ${gradientTo}); color: white; border: none; border-radius: 10px; font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.2s; }
294
- .ihooman-ticket-submit:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(0, 174, 255, 0.3); }
295
- .ihooman-ticket-submit:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
296
- .ihooman-ticket-back { padding: 10px; background: transparent; color: ${mutedColor}; border: 1px solid ${borderColor}; border-radius: 10px; font-size: 13px; cursor: pointer; transition: all 0.2s; }
290
+ .ihooman-ticket-submit { display: flex; align-items: center; justify-content: center; padding: 12px; background: linear-gradient(135deg, ${gradientFrom}, ${gradientTo}); color: white; border: none; border-radius: 8px; font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.2s; line-height: 1.4; }
291
+ .ihooman-ticket-submit:hover { opacity: 0.9; }
292
+ .ihooman-ticket-submit:disabled { opacity: 0.5; cursor: not-allowed; }
293
+ .ihooman-ticket-back { display: flex; align-items: center; justify-content: center; padding: 10px; background: transparent; color: ${mutedColor}; border: 1px solid ${borderColor}; border-radius: 8px; font-size: 13px; cursor: pointer; transition: all 0.2s; line-height: 1.4; }
297
294
  .ihooman-ticket-back:hover { background: ${isDark ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.03)'}; }
298
295
  .ihooman-history-view { display: none; flex-direction: column; flex: 1; overflow: hidden; background: ${bgColor}; }
299
296
  .ihooman-history-view.show { display: flex; }
300
297
  .ihooman-history-header { padding: 12px 16px; border-bottom: 1px solid ${borderColor}; display: flex; justify-content: space-between; align-items: center; }
301
298
  .ihooman-history-title { font-size: 14px; font-weight: 600; color: ${textColor}; margin: 0; }
302
- .ihooman-history-new { display: inline-flex; align-items: center; gap: 4px; background: linear-gradient(135deg, ${gradientFrom}, ${gradientTo}); color: white; border: none; padding: 8px 16px; border-radius: 8px !important; font-size: 13px; font-weight: 500; cursor: pointer; transition: all 0.2s; }
303
- .ihooman-history-new:hover { transform: translateY(-1px); box-shadow: 0 2px 8px rgba(0, 174, 255, 0.3); }
304
- .ihooman-history-new svg { width: 14px; height: 14px; flex-shrink: 0; }
299
+ .ihooman-history-new { display: inline-flex; align-items: center; justify-content: center; gap: 4px; background: linear-gradient(135deg, ${gradientFrom}, ${gradientTo}); color: white; border: none; padding: 6px 12px; border-radius: 6px; font-size: 12px; font-weight: 500; cursor: pointer; transition: all 0.2s; line-height: 1.4; }
300
+ .ihooman-history-new:hover { opacity: 0.9; }
301
+ .ihooman-history-new svg { width: 12px; height: 12px; flex-shrink: 0; }
305
302
  .ihooman-history-list { flex: 1; overflow-y: auto; padding: 8px; overscroll-behavior: contain; }
306
303
  .ihooman-history-item { padding: 12px; border: 1px solid ${borderColor}; border-radius: 8px; margin-bottom: 6px; cursor: pointer; transition: all 0.2s; background: ${bgColor}; }
307
304
  .ihooman-history-item:hover { background: ${isDark ? 'rgba(255,255,255,0.05)' : '#f8fafc'}; }
@@ -312,25 +309,58 @@
312
309
  .ihooman-preset-questions { padding: 10px 16px; display: flex; flex-wrap: wrap; gap: 6px; background: ${bgColor}; border-top: 1px solid ${borderColor}; }
313
310
  .ihooman-preset-questions:empty { display: none; }
314
311
  .ihooman-preset-questions.hidden { display: none !important; }
315
- .ihooman-widget .ihooman-preset-btn { display: inline-flex !important; align-items: center !important; gap: 4px !important; padding: 6px 12px !important; border-radius: 6px !important; border: 1px solid ${borderColor} !important; background: ${isDark ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.02)'} !important; color: ${textColor} !important; font-size: 12px !important; font-weight: 500 !important; cursor: pointer !important; transition: all 0.2s !important; white-space: nowrap !important; width: auto !important; height: auto !important; min-width: 0 !important; aspect-ratio: auto !important; }
316
- .ihooman-widget .ihooman-preset-btn:hover { background: ${isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.05)'} !important; border-color: ${primaryColor} !important; }
317
- .ihooman-widget .ihooman-preset-btn .icon { font-size: 12px !important; flex-shrink: 0 !important; }
318
- @media (max-width: 480px) { .ihooman-window { width: calc(100vw - 20px); height: calc(100vh - 100px); min-height: 400px; max-height: calc(100vh - 100px); left: 10px; right: 10px; bottom: 80px; } .ihooman-toggle { ${positionRight ? 'right: 16px' : 'left: 16px'}; bottom: 16px; } }
312
+ .ihooman-preset-btn { display: inline-flex; align-items: center; justify-content: center; gap: 4px; padding: 6px 10px; border-radius: 6px; border: 1px solid ${borderColor}; background: ${isDark ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.02)'}; color: ${textColor}; font-size: 12px; font-weight: 500; cursor: pointer; transition: all 0.2s; white-space: nowrap; line-height: 1.4; }
313
+ .ihooman-preset-btn:hover { background: ${isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.05)'}; border-color: ${primaryColor}; }
314
+ .ihooman-proactive-toast { position: fixed; ${positionRight ? 'right: 20px' : 'left: 20px'}; ${positionBottom ? 'bottom: 90px' : 'top: 90px'}; max-width: 300px; padding: 16px; background: ${bgColor}; border-radius: 12px; box-shadow: 0 10px 40px rgba(0,0,0,0.15); z-index: ${(zIndex ?? 9999) - 2}; opacity: 0; visibility: hidden; transform: translateY(10px); transition: all 0.3s ease; border: 1px solid ${borderColor}; }
315
+ .ihooman-proactive-toast.show { opacity: 1; visibility: visible; transform: translateY(0); }
316
+ .ihooman-proactive-toast-content { font-size: 14px; color: ${textColor}; margin-bottom: 12px; }
317
+ .ihooman-proactive-toast-actions { display: flex; gap: 8px; }
318
+ .ihooman-proactive-toast-btn { display: inline-flex; align-items: center; justify-content: center; padding: 8px 16px; border-radius: 6px; font-size: 13px; font-weight: 500; cursor: pointer; transition: all 0.2s; line-height: 1.4; }
319
+ .ihooman-proactive-toast-btn.primary { background: linear-gradient(135deg, ${gradientFrom}, ${gradientTo}); color: white; border: none; }
320
+ .ihooman-proactive-toast-btn.primary:hover { opacity: 0.9; }
321
+ .ihooman-proactive-toast-btn.secondary { background: transparent; color: ${mutedColor}; border: 1px solid ${borderColor}; }
322
+ .ihooman-proactive-toast-btn.secondary:hover { background: ${isDark ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.03)'}; }
323
+ .ihooman-survey-view { display: none; flex-direction: column; padding: 20px; gap: 16px; background: ${bgColor}; flex: 1; align-items: center; justify-content: center; }
324
+ .ihooman-survey-view.show { display: flex; }
325
+ .ihooman-survey-question { font-size: 16px; font-weight: 600; color: ${textColor}; text-align: center; }
326
+ .ihooman-survey-stars { display: flex; gap: 8px; }
327
+ .ihooman-survey-star { width: 40px; height: 40px; cursor: pointer; color: ${mutedColor}; transition: all 0.2s; }
328
+ .ihooman-survey-star:hover, .ihooman-survey-star.active { color: #fbbf24; transform: scale(1.1); }
329
+ .ihooman-survey-star svg { width: 100%; height: 100%; }
330
+ .ihooman-survey-comment { width: 100%; padding: 12px; border: 1px solid ${borderColor}; border-radius: 8px; font-size: 14px; resize: none; min-height: 80px; background: ${inputBg}; color: ${textColor}; }
331
+ .ihooman-survey-submit { display: inline-flex; align-items: center; justify-content: center; padding: 10px 20px; background: linear-gradient(135deg, ${gradientFrom}, ${gradientTo}); color: white; border: none; border-radius: 6px; font-size: 13px; font-weight: 500; cursor: pointer; line-height: 1.4; }
332
+ .ihooman-survey-submit:hover { opacity: 0.9; }
333
+ .ihooman-survey-skip { display: inline-flex; align-items: center; justify-content: center; padding: 8px 16px; background: transparent; color: ${mutedColor}; border: none; font-size: 12px; cursor: pointer; line-height: 1.4; }
334
+ .ihooman-survey-skip:hover { color: ${textColor}; }
335
+ .ihooman-feedback-btns { display: flex; gap: 6px; margin-top: 6px; }
336
+ .ihooman-feedback-btn { display: inline-flex; align-items: center; justify-content: center; padding: 4px 6px; border: 1px solid ${borderColor}; border-radius: 4px; background: transparent; cursor: pointer; color: ${mutedColor}; transition: all 0.2s; }
337
+ .ihooman-feedback-btn:hover { background: ${isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.05)'}; }
338
+ .ihooman-feedback-btn.active { background: ${primaryColor}; color: white; border-color: ${primaryColor}; }
339
+ .ihooman-feedback-btn svg { width: 12px; height: 12px; }
340
+ .ihooman-carousel { display: flex; gap: 12px; overflow-x: auto; padding: 8px 0; scroll-snap-type: x mandatory; }
341
+ .ihooman-carousel::-webkit-scrollbar { height: 4px; }
342
+ .ihooman-carousel-card { min-width: 200px; max-width: 250px; border: 1px solid ${borderColor}; border-radius: 12px; overflow: hidden; scroll-snap-align: start; background: ${bgColor}; }
343
+ .ihooman-carousel-card img { width: 100%; height: 120px; object-fit: cover; }
344
+ .ihooman-carousel-card-content { padding: 12px; }
345
+ .ihooman-carousel-card-title { font-size: 14px; font-weight: 600; color: ${textColor}; margin-bottom: 4px; }
346
+ .ihooman-carousel-card-desc { font-size: 12px; color: ${mutedColor}; margin-bottom: 8px; }
347
+ .ihooman-carousel-card-btns { display: flex; flex-direction: column; gap: 6px; }
348
+ .ihooman-carousel-card-btn { display: flex; align-items: center; justify-content: center; padding: 6px 10px; border: 1px solid ${borderColor}; border-radius: 6px; background: transparent; color: ${textColor}; font-size: 11px; cursor: pointer; transition: all 0.2s; text-align: center; line-height: 1.4; }
349
+ .ihooman-carousel-card-btn:hover { background: ${isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.05)'}; border-color: ${primaryColor}; }
350
+ .ihooman-quick-replies { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
351
+ .ihooman-quick-reply { display: inline-flex; align-items: center; justify-content: center; padding: 5px 10px; border: 1px solid ${primaryColor}; border-radius: 14px; background: transparent; color: ${primaryColor}; font-size: 11px; cursor: pointer; transition: all 0.2s; line-height: 1.4; }
352
+ .ihooman-quick-reply:hover { background: ${primaryColor}; color: white; }
353
+ @media (max-width: 480px) { .ihooman-window { width: calc(100vw - 20px); height: calc(100vh - 100px); min-height: 400px; max-height: calc(100vh - 100px); left: 10px; right: 10px; bottom: 80px; } .ihooman-toggle { ${positionRight ? 'right: 16px' : 'left: 16px'}; bottom: 16px; } .ihooman-proactive-toast { left: 10px; right: 10px; max-width: none; } }
319
354
  `;
320
355
  }
321
356
  // ============================================================================
322
357
  // DOM CREATION
323
358
  // ============================================================================
324
- /**
325
- * Create the widget DOM elements
326
- */
327
359
  function createWidget() {
328
- // Add styles
329
360
  const styleEl = document.createElement('style');
330
361
  styleEl.id = 'ihooman-widget-styles';
331
362
  styleEl.textContent = generateStyles();
332
363
  document.head.appendChild(styleEl);
333
- // Create widget container
334
364
  const widget = document.createElement('div');
335
365
  widget.className = 'ihooman-widget';
336
366
  widget.innerHTML = `
@@ -340,6 +370,13 @@
340
370
  <span class="chat-icon">${icons.chat}</span>
341
371
  <span class="close-icon">${icons.close}</span>
342
372
  </button>
373
+ <div class="ihooman-proactive-toast">
374
+ <div class="ihooman-proactive-toast-content"></div>
375
+ <div class="ihooman-proactive-toast-actions">
376
+ <button class="ihooman-proactive-toast-btn primary">Chat Now</button>
377
+ <button class="ihooman-proactive-toast-btn secondary">Dismiss</button>
378
+ </div>
379
+ </div>
343
380
  <div class="ihooman-window" role="dialog" aria-label="Chat window">
344
381
  <div class="ihooman-container">
345
382
  <div class="ihooman-header">
@@ -354,8 +391,6 @@
354
391
  <button class="ihooman-header-btn" data-action="minimize" title="Minimize">${icons.minimize}</button>
355
392
  </div>
356
393
  </div>
357
-
358
- <!-- Chat View -->
359
394
  <div class="ihooman-chat-view">
360
395
  <div class="ihooman-status-bar"></div>
361
396
  <div class="ihooman-messages" role="log" aria-live="polite"></div>
@@ -368,8 +403,6 @@
368
403
  </div>
369
404
  </div>
370
405
  </div>
371
-
372
- <!-- Ticket Form View -->
373
406
  <div class="ihooman-ticket-view">
374
407
  <h4 class="ihooman-ticket-title">📝 Submit a Ticket</h4>
375
408
  <p class="ihooman-ticket-subtitle">We'll get back to you via email</p>
@@ -379,22 +412,25 @@
379
412
  <button class="ihooman-ticket-submit" id="ihooman-ticket-submit">Submit Ticket</button>
380
413
  <button class="ihooman-ticket-back" id="ihooman-ticket-back">← Back to Chat</button>
381
414
  </div>
382
-
383
- <!-- History View -->
384
415
  <div class="ihooman-history-view">
385
416
  <div class="ihooman-history-header">
386
417
  <span class="ihooman-history-title">Your Conversations</span>
387
- <button class="ihooman-history-new" style="all: revert; display: inline-flex !important; align-items: center !important; gap: 4px !important; background: linear-gradient(135deg, ${config.gradientFrom}, ${config.gradientTo}) !important; color: white !important; border: none !important; padding: 6px 10px !important; border-radius: 6px !important; font-size: 12px !important; font-weight: 500 !important; cursor: pointer !important; width: auto !important; height: auto !important; min-width: 0 !important; aspect-ratio: auto !important;">${icons.plus} New Chat</button>
418
+ <button class="ihooman-history-new">${icons.plus} New Chat</button>
388
419
  </div>
389
420
  <div class="ihooman-history-list"></div>
390
421
  </div>
391
-
422
+ <div class="ihooman-survey-view">
423
+ <div class="ihooman-survey-question">${config.surveyConfig?.question || 'How was your experience?'}</div>
424
+ <div class="ihooman-survey-stars"></div>
425
+ <textarea class="ihooman-survey-comment" placeholder="Any additional feedback? (optional)"></textarea>
426
+ <button class="ihooman-survey-submit">Submit Feedback</button>
427
+ <button class="ihooman-survey-skip">Skip</button>
428
+ </div>
392
429
  ${config.poweredBy ? `<div class="ihooman-powered">Powered by <a href="https://ihooman.ai" target="_blank" rel="noopener">Ihooman AI</a></div>` : ''}
393
430
  </div>
394
431
  </div>
395
432
  `;
396
433
  document.body.appendChild(widget);
397
- // Store element references
398
434
  elements = {
399
435
  widget,
400
436
  toggle: widget.querySelector('.ihooman-toggle'),
@@ -403,8 +439,8 @@
403
439
  chatView: widget.querySelector('.ihooman-chat-view'),
404
440
  ticketView: widget.querySelector('.ihooman-ticket-view'),
405
441
  historyView: widget.querySelector('.ihooman-history-view'),
442
+ surveyView: widget.querySelector('.ihooman-survey-view'),
406
443
  historyList: widget.querySelector('.ihooman-history-list'),
407
- historyNewBtn: widget.querySelector('.ihooman-history-new'),
408
444
  messages: widget.querySelector('.ihooman-messages'),
409
445
  presetQuestions: widget.querySelector('.ihooman-preset-questions'),
410
446
  input: widget.querySelector('.ihooman-input'),
@@ -419,22 +455,18 @@
419
455
  ticketIssue: widget.querySelector('#ihooman-ticket-issue'),
420
456
  ticketSubmitBtn: widget.querySelector('#ihooman-ticket-submit'),
421
457
  ticketBackBtn: widget.querySelector('#ihooman-ticket-back'),
458
+ proactiveToast: widget.querySelector('.ihooman-proactive-toast'),
422
459
  };
423
- // Set up event listeners
424
460
  setupEventListeners();
425
461
  }
426
- /**
427
- * Set up DOM event listeners
428
- */
429
462
  function setupEventListeners() {
430
463
  if (!elements.toggle || !elements.sendBtn || !elements.input)
431
464
  return;
432
465
  elements.toggle.addEventListener('click', toggle);
433
466
  elements.sendBtn.addEventListener('click', handleSendClick);
434
467
  elements.input.addEventListener('input', () => {
435
- if (elements.sendBtn) {
468
+ if (elements.sendBtn)
436
469
  elements.sendBtn.disabled = !elements.input?.value.trim();
437
- }
438
470
  if (elements.input) {
439
471
  elements.input.style.height = 'auto';
440
472
  elements.input.style.height = Math.min(elements.input.scrollHeight, 100) + 'px';
@@ -454,27 +486,79 @@
454
486
  elements.widget?.querySelector('[data-action="refresh"]')?.addEventListener('click', startNewConversation);
455
487
  elements.widget?.querySelector('[data-action="minimize"]')?.addEventListener('click', close);
456
488
  elements.widget?.querySelector('[data-action="history"]')?.addEventListener('click', toggleHistoryView);
457
- // Ticket form buttons
458
- if (elements.ticketSubmitBtn) {
489
+ if (elements.ticketSubmitBtn)
459
490
  elements.ticketSubmitBtn.addEventListener('click', handleSubmitTicket);
460
- }
461
- if (elements.ticketBackBtn) {
491
+ if (elements.ticketBackBtn)
462
492
  elements.ticketBackBtn.addEventListener('click', () => showView('chat'));
463
- }
464
- // History view buttons
465
- if (elements.historyNewBtn) {
466
- elements.historyNewBtn.addEventListener('click', () => {
493
+ const historyNewBtn = elements.widget?.querySelector('.ihooman-history-new');
494
+ if (historyNewBtn) {
495
+ historyNewBtn.addEventListener('click', () => {
467
496
  startNewConversation();
468
497
  showView('chat');
469
498
  });
470
499
  }
500
+ // Proactive toast buttons
501
+ const proactivePrimaryBtn = elements.proactiveToast?.querySelector('.ihooman-proactive-toast-btn.primary');
502
+ const proactiveSecondaryBtn = elements.proactiveToast?.querySelector('.ihooman-proactive-toast-btn.secondary');
503
+ if (proactivePrimaryBtn) {
504
+ proactivePrimaryBtn.addEventListener('click', () => {
505
+ hideProactiveToast();
506
+ open();
507
+ emit('proactive:clicked');
508
+ });
509
+ }
510
+ if (proactiveSecondaryBtn) {
511
+ proactiveSecondaryBtn.addEventListener('click', () => {
512
+ hideProactiveToast();
513
+ });
514
+ }
515
+ // Survey buttons
516
+ const surveySubmitBtn = elements.surveyView?.querySelector('.ihooman-survey-submit');
517
+ const surveySkipBtn = elements.surveyView?.querySelector('.ihooman-survey-skip');
518
+ if (surveySubmitBtn)
519
+ surveySubmitBtn.addEventListener('click', handleSurveySubmit);
520
+ if (surveySkipBtn)
521
+ surveySkipBtn.addEventListener('click', () => showView('chat'));
522
+ // Initialize survey stars
523
+ initializeSurveyStars();
524
+ }
525
+ function initializeSurveyStars() {
526
+ const starsContainer = elements.surveyView?.querySelector('.ihooman-survey-stars');
527
+ if (!starsContainer)
528
+ return;
529
+ let selectedRating = 0;
530
+ starsContainer.innerHTML = '';
531
+ for (let i = 1; i <= 5; i++) {
532
+ const star = document.createElement('div');
533
+ star.className = 'ihooman-survey-star';
534
+ star.innerHTML = icons.starEmpty;
535
+ star.dataset.rating = String(i);
536
+ star.addEventListener('click', () => {
537
+ selectedRating = i;
538
+ updateStars(starsContainer, i);
539
+ });
540
+ star.addEventListener('mouseenter', () => updateStars(starsContainer, i));
541
+ star.addEventListener('mouseleave', () => updateStars(starsContainer, selectedRating));
542
+ starsContainer.appendChild(star);
543
+ }
544
+ }
545
+ function updateStars(container, rating) {
546
+ const stars = container.querySelectorAll('.ihooman-survey-star');
547
+ stars.forEach((star, index) => {
548
+ const starEl = star;
549
+ if (index < rating) {
550
+ starEl.innerHTML = icons.star;
551
+ starEl.classList.add('active');
552
+ }
553
+ else {
554
+ starEl.innerHTML = icons.starEmpty;
555
+ starEl.classList.remove('active');
556
+ }
557
+ });
471
558
  }
472
559
  // ============================================================================
473
560
  // MESSAGING
474
561
  // ============================================================================
475
- /**
476
- * Add a message to the chat
477
- */
478
562
  function addMessage(content, sender = 'bot', metadata = {}) {
479
563
  const message = {
480
564
  id: generateId('msg_'),
@@ -488,175 +572,145 @@
488
572
  return message;
489
573
  const el = document.createElement('div');
490
574
  el.className = `ihooman-message ${sender}`;
491
- // Check if this message should show escalation buttons
492
575
  const showEscalationButtons = sender === 'bot' && metadata?.escalation_offered === true;
493
576
  let escalationButtonsHtml = '';
494
577
  if (showEscalationButtons) {
495
- // Use inline styles with !important to override any external CSS
496
- const btnBaseStyle = 'all: revert; display: inline-flex !important; align-items: center !important; justify-content: center !important; gap: 6px !important; padding: 8px 12px !important; border-radius: 6px !important; border: none !important; cursor: pointer !important; font-size: 12px !important; font-weight: 500 !important; line-height: 1.2 !important; width: auto !important; height: auto !important; min-width: 0 !important; min-height: 0 !important; max-width: none !important; max-height: none !important; aspect-ratio: auto !important; box-sizing: border-box !important;';
497
- const primaryStyle = `${btnBaseStyle} background: linear-gradient(135deg, ${config.gradientFrom}, ${config.gradientTo}) !important; color: white !important;`;
498
- const secondaryStyle = `${btnBaseStyle} background: rgba(0,0,0,0.05) !important; color: inherit !important; border: 1px solid rgba(0,0,0,0.1) !important;`;
499
578
  escalationButtonsHtml = `
500
- <div class="ihooman-escalation-actions" style="display: flex !important; gap: 8px !important; margin-top: 10px !important; flex-wrap: wrap !important;">
501
- <button class="ihooman-escalation-btn primary" data-action="live-agent" style="${primaryStyle}">
502
- ${icons.agent}
503
- <span>Talk to Agent</span>
504
- </button>
505
- <button class="ihooman-escalation-btn secondary" data-action="create-ticket" style="${secondaryStyle}">
506
- ${icons.ticket}
507
- <span>Create Ticket</span>
508
- </button>
579
+ <div class="ihooman-escalation-actions">
580
+ <button class="ihooman-escalation-btn primary" data-action="live-agent">${icons.agent}<span>Talk to Agent</span></button>
581
+ <button class="ihooman-escalation-btn secondary" data-action="create-ticket">${icons.ticket}<span>Create Ticket</span></button>
582
+ </div>
583
+ `;
584
+ }
585
+ // Quick replies
586
+ let quickRepliesHtml = '';
587
+ if (metadata?.quick_replies && metadata.quick_replies.length > 0) {
588
+ quickRepliesHtml = `<div class="ihooman-quick-replies">${metadata.quick_replies.map(qr => `<button class="ihooman-quick-reply" data-text="${escapeHtml(qr.text)}">${escapeHtml(qr.text)}</button>`).join('')}</div>`;
589
+ }
590
+ // Feedback buttons for bot messages
591
+ let feedbackHtml = '';
592
+ if (sender === 'bot' && !metadata?.is_system_message) {
593
+ feedbackHtml = `
594
+ <div class="ihooman-feedback-btns">
595
+ <button class="ihooman-feedback-btn" data-feedback="up" title="Helpful">${icons.thumbUp}</button>
596
+ <button class="ihooman-feedback-btn" data-feedback="down" title="Not helpful">${icons.thumbDown}</button>
509
597
  </div>
510
598
  `;
511
599
  }
512
600
  el.innerHTML = `
513
601
  <div class="ihooman-message-content">${parseMarkdown(content)}</div>
514
602
  ${escalationButtonsHtml}
603
+ ${quickRepliesHtml}
604
+ ${feedbackHtml}
515
605
  ${config.showTimestamps ? `<div class="ihooman-message-time">${formatTime(message.timestamp)}</div>` : ''}
516
606
  `;
517
- // Add event listeners for escalation buttons
607
+ // Event listeners
518
608
  if (showEscalationButtons) {
519
- const liveAgentBtn = el.querySelector('[data-action="live-agent"]');
520
- const ticketBtn = el.querySelector('[data-action="create-ticket"]');
521
- if (liveAgentBtn) {
522
- liveAgentBtn.addEventListener('click', () => handleEscalationAction('live-agent'));
523
- }
524
- if (ticketBtn) {
525
- ticketBtn.addEventListener('click', () => handleEscalationAction('create-ticket'));
526
- }
609
+ el.querySelector('[data-action="live-agent"]')?.addEventListener('click', () => handleEscalationAction('live-agent'));
610
+ el.querySelector('[data-action="create-ticket"]')?.addEventListener('click', () => handleEscalationAction('create-ticket'));
527
611
  }
528
- // Remove typing indicator if present
612
+ // Quick reply listeners
613
+ el.querySelectorAll('.ihooman-quick-reply').forEach(btn => {
614
+ btn.addEventListener('click', () => {
615
+ const text = btn.dataset.text;
616
+ if (text) {
617
+ hidePresetQuestions();
618
+ addMessage(text, 'user');
619
+ showTyping();
620
+ sendMessageToServer(text);
621
+ }
622
+ });
623
+ });
624
+ // Feedback listeners
625
+ el.querySelectorAll('.ihooman-feedback-btn').forEach(btn => {
626
+ btn.addEventListener('click', () => {
627
+ const feedback = btn.dataset.feedback;
628
+ el.querySelectorAll('.ihooman-feedback-btn').forEach(b => b.classList.remove('active'));
629
+ btn.classList.add('active');
630
+ submitFeedback(message.id, feedback === 'up' ? 'positive' : 'negative');
631
+ });
632
+ });
529
633
  const typing = elements.messages.querySelector('.ihooman-typing');
530
634
  if (typing)
531
635
  typing.remove();
532
636
  elements.messages.appendChild(el);
533
637
  elements.messages.scrollTop = elements.messages.scrollHeight;
534
- // Update unread count if widget is closed
535
638
  if (sender === 'bot' && !state.isOpen) {
536
639
  state.unreadCount++;
537
- if (elements.badge) {
640
+ if (elements.badge)
538
641
  elements.badge.textContent = String(state.unreadCount);
539
- }
540
642
  if (config.enableSounds)
541
643
  playSound();
542
644
  }
543
645
  emit('message', message);
544
646
  return message;
545
647
  }
546
- /**
547
- * Render preset questions in the widget
548
- */
549
648
  function renderPresetQuestions() {
649
+ console.debug('[IhoomanChat] Rendering preset questions:', presetQuestions.length);
550
650
  if (!elements.presetQuestions || presetQuestions.length === 0) {
551
- if (elements.presetQuestions) {
651
+ if (elements.presetQuestions)
552
652
  elements.presetQuestions.innerHTML = '';
553
- }
554
653
  return;
555
654
  }
556
655
  elements.presetQuestions.innerHTML = presetQuestions.map(q => `
557
656
  <button class="ihooman-preset-btn" data-question-id="${escapeHtml(q.id)}" data-question-text="${escapeHtml(q.text)}">
558
- ${q.icon ? `<span class="icon">${escapeHtml(q.icon)}</span>` : ''}
657
+ ${q.icon || q.emoji ? `<span class="icon">${escapeHtml(q.icon || q.emoji || '')}</span>` : ''}
559
658
  <span>${escapeHtml(q.text)}</span>
560
659
  </button>
561
660
  `).join('');
562
- // Add click handlers
563
661
  elements.presetQuestions.querySelectorAll('.ihooman-preset-btn').forEach(btn => {
564
662
  btn.addEventListener('click', () => {
565
663
  const questionText = btn.dataset.questionText;
566
- if (questionText) {
664
+ if (questionText)
567
665
  handlePresetQuestionClick(questionText);
568
- }
569
666
  });
570
667
  });
571
668
  }
572
- /**
573
- * Handle preset question click - send the question as a message
574
- */
575
669
  function handlePresetQuestionClick(questionText) {
576
- // Hide preset questions immediately
577
670
  hidePresetQuestions();
578
- // Add the question as a user message
579
671
  addMessage(questionText, 'user');
580
- // Show typing indicator
581
672
  showTyping();
582
- // Send to server
583
673
  sendMessageToServer(questionText);
584
674
  }
585
- /**
586
- * Handle escalation action button clicks
587
- */
588
675
  function handleEscalationAction(action) {
589
- // Disable all escalation buttons to prevent double-clicks
590
676
  const buttons = document.querySelectorAll('.ihooman-escalation-btn');
591
677
  buttons.forEach(btn => btn.disabled = true);
592
- if (action === 'live-agent') {
678
+ if (action === 'live-agent')
593
679
  handleRequestLiveAgent();
594
- }
595
- else if (action === 'create-ticket') {
596
- handleShowTicketForm();
597
- }
680
+ else if (action === 'create-ticket')
681
+ showView('ticket');
598
682
  }
599
- /**
600
- * Switch between chat, ticket, and history views
601
- */
602
683
  function showView(view) {
603
684
  currentView = view;
604
- if (elements.chatView) {
685
+ state.view = view;
686
+ if (elements.chatView)
605
687
  elements.chatView.classList.toggle('hidden', view !== 'chat');
606
- }
607
- if (elements.ticketView) {
688
+ if (elements.ticketView)
608
689
  elements.ticketView.classList.toggle('show', view === 'ticket');
609
- }
610
- if (elements.historyView) {
690
+ if (elements.historyView)
611
691
  elements.historyView.classList.toggle('show', view === 'history');
612
- }
613
- // Load history when showing history view
614
- if (view === 'history') {
692
+ if (elements.surveyView)
693
+ elements.surveyView.classList.toggle('show', view === 'survey');
694
+ if (view === 'history')
615
695
  loadConversationHistory();
616
- }
617
696
  }
618
- /**
619
- * Toggle between chat and history views
620
- */
621
697
  function toggleHistoryView() {
622
- if (currentView === 'history') {
623
- showView('chat');
624
- }
625
- else {
626
- showView('history');
627
- }
628
- }
629
- /**
630
- * Format time ago string
631
- */
632
- function timeAgo(date) {
633
- const diff = Date.now() - new Date(date).getTime();
634
- if (diff < 60000)
635
- return 'now';
636
- if (diff < 3600000)
637
- return Math.floor(diff / 60000) + 'm';
638
- if (diff < 86400000)
639
- return Math.floor(diff / 3600000) + 'h';
640
- return Math.floor(diff / 86400000) + 'd';
698
+ showView(currentView === 'history' ? 'chat' : 'history');
641
699
  }
642
- /**
643
- * Load conversation history from the server
644
- */
645
700
  async function loadConversationHistory() {
646
701
  if (!elements.historyList || !state.visitorId)
647
702
  return;
648
703
  elements.historyList.innerHTML = '<div class="ihooman-history-empty">Loading...</div>';
649
704
  try {
650
705
  const response = await fetch(`${config.serverUrl}/api/widget/conversations?widget_id=${config.widgetId}&visitor_id=${state.visitorId}&limit=20`);
651
- if (!response.ok) {
706
+ if (!response.ok)
652
707
  throw new Error('Failed to load history');
653
- }
654
708
  const conversations = await response.json();
655
709
  if (!conversations.length) {
656
710
  elements.historyList.innerHTML = '<div class="ihooman-history-empty">No conversations yet</div>';
657
711
  return;
658
712
  }
659
- elements.historyList.innerHTML = conversations.map(conv => `
713
+ elements.historyList.innerHTML = conversations.map((conv) => `
660
714
  <div class="ihooman-history-item ${conv.session_id === state.sessionId ? 'active' : ''}" data-session-id="${conv.session_id}">
661
715
  <div class="ihooman-history-preview">${escapeHtml(conv.preview || 'New conversation')}</div>
662
716
  <div class="ihooman-history-meta">
@@ -665,13 +719,11 @@
665
719
  </div>
666
720
  </div>
667
721
  `).join('');
668
- // Add click handlers to history items
669
722
  elements.historyList.querySelectorAll('.ihooman-history-item').forEach(item => {
670
723
  item.addEventListener('click', () => {
671
724
  const sessionId = item.dataset.sessionId;
672
- if (sessionId) {
725
+ if (sessionId)
673
726
  switchToConversation(sessionId);
674
- }
675
727
  });
676
728
  });
677
729
  }
@@ -679,49 +731,35 @@
679
731
  console.error('Error loading conversation history:', error);
680
732
  elements.historyList.innerHTML = '<div class="ihooman-history-empty">Failed to load history</div>';
681
733
  }
734
+ finally {
735
+ }
682
736
  }
683
- /**
684
- * Switch to a specific conversation
685
- */
686
737
  async function switchToConversation(sessionId) {
687
738
  state.sessionId = sessionId;
688
- if (config.persistSession) {
739
+ if (config.persistSession)
689
740
  storage('session_id', sessionId);
690
- }
691
- // Clear current messages
692
- if (elements.messages) {
741
+ if (elements.messages)
693
742
  elements.messages.innerHTML = '';
694
- }
695
- // Reset live agent mode
696
743
  isLiveAgentMode = false;
697
744
  stopLiveAgentPolling();
698
745
  updateStatusBar('hidden');
699
- // Reconnect WebSocket with new session
700
746
  intentionalDisconnect = true;
701
747
  if (ws) {
702
748
  ws.close();
703
749
  ws = null;
704
750
  }
705
751
  connectWebSocket();
706
- // Switch to chat view
707
752
  showView('chat');
708
- // Load conversation messages
709
753
  await loadConversationMessages(sessionId);
710
754
  }
711
- /**
712
- * Load messages for a specific conversation
713
- */
714
755
  async function loadConversationMessages(sessionId) {
715
756
  try {
716
757
  const response = await fetch(`${config.serverUrl}/api/widget/transcript/${sessionId}?widget_id=${config.widgetId}`);
717
- if (!response.ok) {
758
+ if (!response.ok)
718
759
  throw new Error('Failed to load messages');
719
- }
720
760
  const data = await response.json();
721
- if (elements.messages) {
761
+ if (elements.messages)
722
762
  elements.messages.innerHTML = '';
723
- }
724
- // Add messages to the chat
725
763
  if (data.messages && data.messages.length > 0) {
726
764
  data.messages.forEach((msg) => {
727
765
  const sender = msg.sender_type === 'user' ? 'user' : 'bot';
@@ -731,34 +769,18 @@
731
769
  else if (config.welcomeMessage) {
732
770
  addMessage(config.welcomeMessage, 'bot');
733
771
  }
734
- // Check conversation status
735
772
  if (data.status === 'pending') {
736
773
  isLiveAgentMode = true;
737
774
  startLiveAgentPolling();
738
775
  updateStatusBar('waiting', '⏳ Waiting for agent...');
739
776
  }
740
- else if (data.status === 'closed') {
741
- updateStatusBar('hidden');
742
- }
743
777
  }
744
778
  catch (error) {
745
779
  console.error('Error loading conversation messages:', error);
746
- if (config.welcomeMessage) {
780
+ if (config.welcomeMessage)
747
781
  addMessage(config.welcomeMessage, 'bot');
748
- }
749
782
  }
750
783
  }
751
- /**
752
- * Show the ticket form
753
- */
754
- function handleShowTicketForm() {
755
- showView('ticket');
756
- // Focus on name input
757
- setTimeout(() => elements.ticketName?.focus(), 100);
758
- }
759
- /**
760
- * Update the status bar display
761
- */
762
784
  function updateStatusBar(status, message) {
763
785
  if (!elements.statusBar)
764
786
  return;
@@ -769,13 +791,9 @@
769
791
  elements.statusBar.classList.add('show');
770
792
  elements.statusBar.classList.remove('waiting', 'connected');
771
793
  elements.statusBar.classList.add(status);
772
- if (message) {
794
+ if (message)
773
795
  elements.statusBar.textContent = message;
774
- }
775
796
  }
776
- /**
777
- * Submit a ticket via the API
778
- */
779
797
  async function handleSubmitTicket() {
780
798
  const name = elements.ticketName?.value.trim();
781
799
  const email = elements.ticketEmail?.value.trim();
@@ -789,7 +807,6 @@
789
807
  elements.ticketSubmitBtn.textContent = 'Submitting...';
790
808
  }
791
809
  try {
792
- // Use Widget ID authenticated endpoint
793
810
  const response = await fetch(`${config.serverUrl}/api/widget/submit-ticket`, {
794
811
  method: 'POST',
795
812
  headers: { 'Content-Type': 'application/json' },
@@ -802,16 +819,13 @@
802
819
  }),
803
820
  });
804
821
  const data = await response.json();
805
- // Clear form
806
822
  if (elements.ticketName)
807
823
  elements.ticketName.value = '';
808
824
  if (elements.ticketEmail)
809
825
  elements.ticketEmail.value = '';
810
826
  if (elements.ticketIssue)
811
827
  elements.ticketIssue.value = '';
812
- // Switch back to chat view
813
828
  showView('chat');
814
- // Show success message
815
829
  const ticketRef = data.ticket_id ? data.ticket_id.slice(0, 8) : 'submitted';
816
830
  addMessage(`✅ Ticket submitted! We'll contact you at ${email}. Reference: #${ticketRef}`, 'bot', { is_system_message: true });
817
831
  }
@@ -824,22 +838,12 @@
824
838
  elements.ticketSubmitBtn.textContent = 'Submit Ticket';
825
839
  }
826
840
  }
827
- /**
828
- * Request a live agent via the API
829
- */
830
841
  async function handleRequestLiveAgent() {
831
842
  if (!state.sessionId) {
832
843
  addMessage('Please send a message first to start a conversation.', 'bot', { is_system_message: true });
833
844
  return;
834
845
  }
835
- // Disable live agent button
836
- const liveBtn = elements.widget?.querySelector('[data-action="live-agent"]');
837
- if (liveBtn) {
838
- liveBtn.disabled = true;
839
- liveBtn.innerHTML = '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg> Connecting...';
840
- }
841
846
  try {
842
- // Use Widget ID authenticated endpoint
843
847
  const response = await fetch(`${config.serverUrl}/api/widget/live-agent`, {
844
848
  method: 'POST',
845
849
  headers: { 'Content-Type': 'application/json' },
@@ -851,33 +855,21 @@
851
855
  });
852
856
  const data = await response.json();
853
857
  isLiveAgentMode = true;
854
- // Show status bar - always start with waiting since agent hasn't accepted yet
858
+ state.escalationStatus = { active: true, type: 'live_agent', queuePosition: data.position_in_queue };
855
859
  if (data.position_in_queue && data.position_in_queue > 1) {
856
860
  updateStatusBar('waiting', `⏳ Waiting for agent (Position: #${data.position_in_queue})`);
857
861
  }
858
862
  else {
859
863
  updateStatusBar('waiting', '⏳ Connecting to live support...');
860
864
  }
861
- // Start polling for agent messages
862
865
  startLiveAgentPolling();
866
+ emit('escalation:start', { type: 'live_agent' });
863
867
  }
864
868
  catch (error) {
865
869
  console.error('Error requesting live agent:', error);
866
870
  addMessage('❌ Unable to connect to live support. Please try again.', 'bot', { is_system_message: true });
867
871
  }
868
- // Re-enable button
869
- if (liveBtn) {
870
- liveBtn.disabled = false;
871
- liveBtn.innerHTML = '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg> Live Agent';
872
- }
873
872
  }
874
- /**
875
- * Live agent polling interval
876
- */
877
- let liveAgentPollInterval = null;
878
- /**
879
- * Start polling for live agent messages
880
- */
881
873
  function startLiveAgentPolling() {
882
874
  if (liveAgentPollInterval)
883
875
  return;
@@ -887,58 +879,102 @@
887
879
  return;
888
880
  }
889
881
  try {
890
- // Note: transcript endpoint still uses API key auth, but we can use the WebSocket
891
- // for real-time updates. For now, poll the escalation status endpoint.
892
- // Update queue position and connection status using Widget ID auth
893
- try {
894
- const escResponse = await fetch(`${config.serverUrl}/api/widget/escalation-status/${state.sessionId}?widget_id=${config.widgetId}`);
895
- if (escResponse.ok) {
896
- const escData = await escResponse.json();
897
- if (escData.escalated) {
898
- if (escData.ticket_status === 'in_progress') {
899
- // Agent has accepted - show connected status
900
- updateStatusBar('connected', '🟢 Connected to live agent');
901
- }
902
- else if (escData.ticket_status === 'open') {
903
- // Still waiting in queue
904
- if (escData.position_in_queue && escData.position_in_queue > 1) {
905
- updateStatusBar('waiting', `⏳ Waiting for agent (Position: #${escData.position_in_queue})`);
906
- }
907
- else {
908
- updateStatusBar('waiting', '⏳ Waiting for agent...');
909
- }
910
- }
911
- else if (escData.ticket_status === 'closed' || escData.ticket_status === 'resolved') {
912
- // Conversation closed
913
- isLiveAgentMode = false;
914
- stopLiveAgentPolling();
915
- updateStatusBar('hidden');
916
- addMessage('This conversation has been closed. Thank you for contacting us!', 'bot', { is_system_message: true });
917
- }
882
+ const response = await fetch(`${config.serverUrl}/api/widget/escalation-status/${state.sessionId}?widget_id=${config.widgetId}`);
883
+ if (response.ok) {
884
+ const data = await response.json();
885
+ if (data.escalated) {
886
+ if (data.ticket_status === 'in_progress') {
887
+ updateStatusBar('connected', '🟢 Connected to live agent');
888
+ }
889
+ else if (data.ticket_status === 'open') {
890
+ updateStatusBar('waiting', data.position_in_queue > 1
891
+ ? `⏳ Waiting for agent (Position: #${data.position_in_queue})`
892
+ : ' Waiting for agent...');
893
+ }
894
+ else if (data.ticket_status === 'closed' || data.ticket_status === 'resolved') {
895
+ isLiveAgentMode = false;
896
+ stopLiveAgentPolling();
897
+ updateStatusBar('hidden');
898
+ addMessage('This conversation has been closed. Thank you for contacting us!', 'bot', { is_system_message: true });
899
+ emit('escalation:end', { type: 'live_agent' });
918
900
  }
919
901
  }
920
902
  }
921
- catch {
922
- // Ignore escalation status errors
923
- }
924
- }
925
- catch (error) {
926
- console.error('Error polling for messages:', error);
927
903
  }
904
+ catch { /* ignore */ }
928
905
  }, 3000);
929
906
  }
930
- /**
931
- * Stop live agent polling
932
- */
933
907
  function stopLiveAgentPolling() {
934
908
  if (liveAgentPollInterval) {
935
909
  clearInterval(liveAgentPollInterval);
936
910
  liveAgentPollInterval = null;
937
911
  }
938
912
  }
913
+ async function submitFeedback(messageId, feedbackType) {
914
+ try {
915
+ await fetch(`${config.serverUrl}/api/widget/feedback`, {
916
+ method: 'POST',
917
+ headers: { 'Content-Type': 'application/json' },
918
+ body: JSON.stringify({
919
+ widget_id: config.widgetId,
920
+ message_id: messageId,
921
+ session_id: state.sessionId,
922
+ feedback_type: feedbackType,
923
+ }),
924
+ });
925
+ }
926
+ catch (error) {
927
+ console.error('Error submitting feedback:', error);
928
+ }
929
+ }
930
+ async function handleSurveySubmit() {
931
+ const starsContainer = elements.surveyView?.querySelector('.ihooman-survey-stars');
932
+ const activeStars = starsContainer?.querySelectorAll('.ihooman-survey-star.active');
933
+ const rating = activeStars?.length || 0;
934
+ const comment = elements.surveyView?.querySelector('.ihooman-survey-comment')?.value;
935
+ if (rating === 0) {
936
+ alert('Please select a rating');
937
+ return;
938
+ }
939
+ try {
940
+ await fetch(`${config.serverUrl}/api/widget/survey-response`, {
941
+ method: 'POST',
942
+ headers: { 'Content-Type': 'application/json' },
943
+ body: JSON.stringify({
944
+ widget_id: config.widgetId,
945
+ survey_id: config.surveyConfig?.id,
946
+ session_id: state.sessionId,
947
+ rating,
948
+ comment,
949
+ }),
950
+ });
951
+ // Mark survey as completed for this session
952
+ storage('survey_completed_' + state.sessionId, true);
953
+ emit('survey:submitted', { rating, comment });
954
+ addMessage(config.surveyConfig?.thankYouMessage || 'Thank you for your feedback!', 'bot', { is_system_message: true });
955
+ showView('chat');
956
+ }
957
+ catch (error) {
958
+ console.error('Error submitting survey:', error);
959
+ }
960
+ }
939
961
  /**
940
- * Show typing indicator
962
+ * Check if survey should be shown and show it
941
963
  */
964
+ function checkAndShowSurvey() {
965
+ // Don't show if no survey configured
966
+ if (!config.surveyConfig)
967
+ return;
968
+ // Don't show if already completed for this session
969
+ if (state.sessionId && storage('survey_completed_' + state.sessionId))
970
+ return;
971
+ // Don't show if not enough messages (at least 2 exchanges)
972
+ if (state.messages.length < 4)
973
+ return;
974
+ // Show the survey
975
+ showView('survey');
976
+ emit('survey:shown', config.surveyConfig);
977
+ }
942
978
  function showTyping() {
943
979
  if (!config.showTypingIndicator || !elements.messages)
944
980
  return;
@@ -950,56 +986,35 @@
950
986
  elements.messages.appendChild(typing);
951
987
  elements.messages.scrollTop = elements.messages.scrollHeight;
952
988
  }
953
- /**
954
- * Hide typing indicator
955
- */
956
989
  function hideTyping() {
957
990
  const typing = elements.messages?.querySelector('.ihooman-typing');
958
991
  if (typing)
959
992
  typing.remove();
960
993
  }
961
- /**
962
- * Handle send button click
963
- */
964
994
  function handleSendClick() {
965
995
  const content = elements.input?.value.trim();
966
996
  if (!content)
967
997
  return;
968
- // Hide preset questions after user sends any message
969
998
  hidePresetQuestions();
970
999
  if (elements.input) {
971
1000
  elements.input.value = '';
972
1001
  elements.input.style.height = 'auto';
973
1002
  }
974
- if (elements.sendBtn) {
1003
+ if (elements.sendBtn)
975
1004
  elements.sendBtn.disabled = true;
976
- }
977
1005
  addMessage(content, 'user');
978
1006
  showTyping();
979
1007
  sendMessageToServer(content);
980
1008
  }
981
- /**
982
- * Hide preset questions
983
- */
984
1009
  function hidePresetQuestions() {
985
- if (elements.presetQuestions) {
1010
+ if (elements.presetQuestions)
986
1011
  elements.presetQuestions.classList.add('hidden');
987
- }
988
1012
  }
989
- /**
990
- * Send message to the server
991
- * Uses WebSocket if connected, otherwise falls back to REST API
992
- */
993
1013
  async function sendMessageToServer(content) {
994
- // If WebSocket is connected, send via WebSocket
995
1014
  if (ws && ws.readyState === WebSocket.OPEN) {
996
- ws.send(JSON.stringify({
997
- type: 'message',
998
- content: content,
999
- }));
1015
+ ws.send(JSON.stringify({ type: 'message', content }));
1000
1016
  return;
1001
1017
  }
1002
- // Fallback to REST API
1003
1018
  try {
1004
1019
  const response = await fetch(`${config.serverUrl}/api/v1/public/chat`, {
1005
1020
  method: 'POST',
@@ -1019,7 +1034,12 @@
1019
1034
  storage('session_id', state.sessionId);
1020
1035
  }
1021
1036
  if (data.response) {
1022
- addMessage(data.response, 'bot', { sources: data.sources, confidence: data.confidence });
1037
+ addMessage(data.response, 'bot', {
1038
+ sources: data.sources,
1039
+ confidence: data.confidence,
1040
+ escalation_offered: data.escalation_offered,
1041
+ quick_replies: data.quick_replies,
1042
+ });
1023
1043
  }
1024
1044
  }
1025
1045
  catch (error) {
@@ -1028,9 +1048,6 @@
1028
1048
  emit('error', error);
1029
1049
  }
1030
1050
  }
1031
- /**
1032
- * Handle file selection
1033
- */
1034
1051
  function handleFileSelect(e) {
1035
1052
  const target = e.target;
1036
1053
  const file = target.files?.[0];
@@ -1056,102 +1073,179 @@
1056
1073
  });
1057
1074
  target.value = '';
1058
1075
  }
1059
- /**
1060
- * Play notification sound
1061
- */
1062
1076
  function playSound() {
1063
1077
  try {
1064
1078
  const audio = new Audio('data:audio/mp3;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU4Ljc2LjEwMAAAAAAAAAAAAAAA//tQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWGluZwAAAA8AAAACAAABhgC7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7//////////////////////////////////////////////////////////////////8AAAAATGF2YzU4LjEzAAAAAAAAAAAAAAAAJAAAAAAAAAAAAYYNBrP/AAAAAAAAAAAAAAAAAAAAAP/7UMQAA8AAAaQAAAAgAAA0gAAABExBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//tQxBKDwAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVU=');
1065
1079
  audio.volume = 0.3;
1066
1080
  audio.play().catch(() => { });
1067
1081
  }
1068
- catch {
1069
- // Ignore audio errors
1082
+ catch { /* ignore */ }
1083
+ }
1084
+ // ============================================================================
1085
+ // PROACTIVE MESSAGES
1086
+ // ============================================================================
1087
+ function initializeProactiveMessages() {
1088
+ if (!config.proactiveMessages || config.proactiveMessages.length === 0) {
1089
+ console.debug('[IhoomanChat] No proactive messages configured');
1090
+ return;
1091
+ }
1092
+ console.debug('[IhoomanChat] Initializing proactive messages:', config.proactiveMessages.length);
1093
+ // Load shown proactive IDs from storage
1094
+ shownProactiveIds = storage('shown_proactive') || [];
1095
+ proactiveCooldowns = storage('proactive_cooldowns') || {};
1096
+ // Start checking for triggers
1097
+ proactiveCheckInterval = setInterval(checkProactiveTriggers, 5000);
1098
+ // Also check on scroll
1099
+ window.addEventListener('scroll', checkProactiveTriggers);
1100
+ // Exit intent detection
1101
+ document.addEventListener('mouseout', (e) => {
1102
+ if (e.clientY <= 0)
1103
+ checkExitIntentTrigger();
1104
+ });
1105
+ }
1106
+ function checkProactiveTriggers() {
1107
+ if (state.isOpen || !config.proactiveMessages)
1108
+ return;
1109
+ const now = Date.now();
1110
+ for (const pm of config.proactiveMessages) {
1111
+ // Skip if already shown in this session
1112
+ if (shownProactiveIds.includes(pm.id))
1113
+ continue;
1114
+ // Check cooldown
1115
+ const lastShown = proactiveCooldowns[pm.id];
1116
+ if (lastShown && now - lastShown < pm.cooldownMinutes * 60 * 1000)
1117
+ continue;
1118
+ // Check date range
1119
+ if (pm.startDate && new Date(pm.startDate) > new Date())
1120
+ continue;
1121
+ if (pm.endDate && new Date(pm.endDate) < new Date())
1122
+ continue;
1123
+ // Check trigger
1124
+ let triggered = false;
1125
+ switch (pm.trigger.type) {
1126
+ case 'time':
1127
+ const timeValue = typeof pm.trigger.value === 'number' ? pm.trigger.value : parseInt(String(pm.trigger.value), 10);
1128
+ const pageLoadTime = storage('page_load_time') || now;
1129
+ if (now - pageLoadTime >= timeValue * 1000)
1130
+ triggered = true;
1131
+ break;
1132
+ case 'scroll':
1133
+ const scrollValue = typeof pm.trigger.value === 'number' ? pm.trigger.value : parseInt(String(pm.trigger.value), 10);
1134
+ const currentScroll = getCurrentScrollDepth();
1135
+ if (currentScroll >= scrollValue)
1136
+ triggered = true;
1137
+ break;
1138
+ case 'url_pattern':
1139
+ if (matchUrlPattern(String(pm.trigger.value)))
1140
+ triggered = true;
1141
+ break;
1142
+ }
1143
+ if (triggered) {
1144
+ showProactiveMessage(pm);
1145
+ break;
1146
+ }
1070
1147
  }
1071
1148
  }
1149
+ function checkExitIntentTrigger() {
1150
+ if (state.isOpen || !config.proactiveMessages)
1151
+ return;
1152
+ const exitIntentMessages = config.proactiveMessages.filter(pm => pm.trigger.type === 'exit_intent' && !shownProactiveIds.includes(pm.id));
1153
+ if (exitIntentMessages.length > 0) {
1154
+ showProactiveMessage(exitIntentMessages[0]);
1155
+ }
1156
+ }
1157
+ function showProactiveMessage(pm) {
1158
+ // Mark as shown
1159
+ shownProactiveIds.push(pm.id);
1160
+ proactiveCooldowns[pm.id] = Date.now();
1161
+ storage('shown_proactive', shownProactiveIds);
1162
+ storage('proactive_cooldowns', proactiveCooldowns);
1163
+ // Show toast
1164
+ if (elements.proactiveToast) {
1165
+ const content = elements.proactiveToast.querySelector('.ihooman-proactive-toast-content');
1166
+ if (content)
1167
+ content.textContent = pm.message;
1168
+ elements.proactiveToast.classList.add('show');
1169
+ emit('proactive:shown', pm);
1170
+ // Auto-open if configured
1171
+ if (pm.autoOpen) {
1172
+ setTimeout(() => {
1173
+ hideProactiveToast();
1174
+ open();
1175
+ }, 3000);
1176
+ }
1177
+ }
1178
+ }
1179
+ function hideProactiveToast() {
1180
+ if (elements.proactiveToast) {
1181
+ elements.proactiveToast.classList.remove('show');
1182
+ }
1183
+ }
1184
+ function cleanupProactiveMessages() {
1185
+ if (proactiveCheckInterval) {
1186
+ clearInterval(proactiveCheckInterval);
1187
+ proactiveCheckInterval = null;
1188
+ }
1189
+ window.removeEventListener('scroll', checkProactiveTriggers);
1190
+ }
1072
1191
  // ============================================================================
1073
1192
  // WEBSOCKET CONNECTION
1074
1193
  // ============================================================================
1075
- /**
1076
- * Connect to WebSocket for real-time messaging.
1077
- *
1078
- * For public widgets (using Widget ID), connects to /public/ws endpoint
1079
- * which authenticates via Widget ID + Domain validation.
1080
- *
1081
- * For authenticated users (dashboard), connects to /ws endpoint with JWT.
1082
- */
1083
1194
  function connectWebSocket(chatEndpoint) {
1084
1195
  if (!config.serverUrl && !chatEndpoint)
1085
1196
  return;
1086
1197
  let wsUrl;
1087
1198
  if (config.widgetId) {
1088
- // Public widget - use /public/ws endpoint with widget_id auth
1089
1199
  const baseWsUrl = config.serverUrl?.replace(/^http/, 'ws');
1090
- const params = new URLSearchParams({
1091
- widget_id: config.widgetId,
1092
- });
1093
- if (state.visitorId) {
1200
+ const params = new URLSearchParams({ widget_id: config.widgetId });
1201
+ if (state.visitorId)
1094
1202
  params.append('visitor_id', state.visitorId);
1095
- }
1096
- if (state.sessionId) {
1203
+ if (state.sessionId)
1097
1204
  params.append('session_id', state.sessionId);
1098
- }
1099
1205
  wsUrl = `${baseWsUrl}/public/ws?${params.toString()}`;
1100
1206
  }
1101
1207
  else {
1102
- // Authenticated user - use /ws endpoint
1103
1208
  wsUrl = chatEndpoint || config.serverUrl?.replace(/^http/, 'ws') + '/ws';
1104
1209
  }
1105
1210
  try {
1106
- // Stop any existing heartbeat before creating new connection
1107
1211
  stopHeartbeat();
1108
1212
  ws = new WebSocket(wsUrl);
1109
1213
  ws.onopen = () => {
1110
1214
  state.isConnected = true;
1215
+ state.connectionStatus = 'connected';
1111
1216
  reconnectAttempts = 0;
1112
1217
  updateStatus(true);
1113
1218
  emit('connected');
1114
- // Start heartbeat after connection is established
1115
1219
  startHeartbeat();
1116
1220
  };
1117
1221
  ws.onclose = (event) => {
1118
1222
  state.isConnected = false;
1223
+ state.connectionStatus = 'disconnected';
1119
1224
  updateStatus(false);
1120
1225
  stopHeartbeat();
1121
1226
  emit('disconnected');
1122
- // Log close reason for debugging
1123
- console.log(`WebSocket closed: code=${event.code}, reason=${event.reason || 'none'}, wasClean=${event.wasClean}`);
1124
- // Don't reconnect if this was an intentional disconnect (e.g., starting new conversation)
1125
1227
  if (intentionalDisconnect) {
1126
1228
  intentionalDisconnect = false;
1127
1229
  return;
1128
1230
  }
1129
- // Don't reconnect if the widget is closed
1130
- if (!state.isOpen) {
1231
+ if (!state.isOpen)
1131
1232
  return;
1132
- }
1133
- // Attempt reconnection with exponential backoff
1134
1233
  if (reconnectAttempts < maxReconnectAttempts) {
1135
1234
  reconnectAttempts++;
1136
1235
  const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
1137
- console.log(`WebSocket reconnecting in ${delay}ms (attempt ${reconnectAttempts}/${maxReconnectAttempts})`);
1138
1236
  setTimeout(() => connectWebSocket(chatEndpoint), delay);
1139
1237
  }
1140
1238
  else {
1141
- // Fall back to polling after max reconnect attempts
1142
- console.warn('WebSocket reconnection failed, falling back to polling');
1143
1239
  startPolling();
1144
1240
  }
1145
1241
  };
1146
1242
  ws.onerror = (error) => {
1147
1243
  console.error('WebSocket error:', error);
1148
- // WebSocket error - will trigger onclose
1149
1244
  };
1150
1245
  ws.onmessage = (e) => {
1151
1246
  try {
1152
1247
  const data = JSON.parse(e.data);
1153
1248
  if (data.type === 'connected') {
1154
- // Server confirmed connection - update session info
1155
1249
  if (data.session_id) {
1156
1250
  state.sessionId = data.session_id;
1157
1251
  if (config.persistSession)
@@ -1162,7 +1256,6 @@
1162
1256
  if (config.persistSession)
1163
1257
  storage('visitor_id', state.visitorId);
1164
1258
  }
1165
- // Show welcome message if provided
1166
1259
  if (data.welcome_message && state.messages.length === 0) {
1167
1260
  addMessage(data.welcome_message, 'bot');
1168
1261
  }
@@ -1175,64 +1268,44 @@
1175
1268
  agent_name: data.agent_name,
1176
1269
  escalation_offered: data.escalation_offered,
1177
1270
  escalated: data.escalated,
1271
+ quick_replies: data.quick_replies,
1178
1272
  });
1179
1273
  }
1180
1274
  else if (data.type === 'typing') {
1181
1275
  data.is_typing ? showTyping() : hideTyping();
1182
1276
  }
1183
1277
  else if (data.type === 'pong') {
1184
- // Heartbeat response - connection is alive
1278
+ // Heartbeat response
1185
1279
  }
1186
1280
  else if (data.type === 'error') {
1187
1281
  console.error('WebSocket server error:', data.message);
1188
1282
  emit('error', { message: data.message });
1189
1283
  }
1190
1284
  }
1191
- catch {
1192
- // Ignore parse errors
1193
- }
1285
+ catch { /* ignore */ }
1194
1286
  };
1195
1287
  }
1196
1288
  catch {
1197
- // WebSocket not supported, fall back to polling
1198
- console.warn('WebSocket not supported, using polling');
1199
1289
  startPolling();
1200
1290
  }
1201
1291
  }
1202
- /**
1203
- * Heartbeat interval reference
1204
- */
1205
- let heartbeatInterval = null;
1206
- /**
1207
- * Start heartbeat to keep WebSocket connection alive
1208
- * Sends ping every 25 seconds to prevent Cloudflare/proxy timeouts
1209
- */
1210
1292
  function startHeartbeat() {
1211
- // Clear any existing heartbeat first
1212
1293
  stopHeartbeat();
1213
1294
  heartbeatInterval = setInterval(() => {
1214
1295
  if (ws && ws.readyState === WebSocket.OPEN) {
1215
1296
  try {
1216
1297
  ws.send(JSON.stringify({ type: 'ping' }));
1217
1298
  }
1218
- catch (e) {
1219
- console.warn('Failed to send heartbeat ping:', e);
1220
- }
1299
+ catch { /* ignore */ }
1221
1300
  }
1222
- }, 25000); // Ping every 25 seconds (Cloudflare timeout is 100s, but some proxies are shorter)
1301
+ }, 25000);
1223
1302
  }
1224
- /**
1225
- * Stop heartbeat
1226
- */
1227
1303
  function stopHeartbeat() {
1228
1304
  if (heartbeatInterval) {
1229
1305
  clearInterval(heartbeatInterval);
1230
1306
  heartbeatInterval = null;
1231
1307
  }
1232
1308
  }
1233
- /**
1234
- * Start polling for messages (fallback when WebSocket unavailable)
1235
- */
1236
1309
  function startPolling() {
1237
1310
  if (pollInterval)
1238
1311
  return;
@@ -1250,132 +1323,107 @@
1250
1323
  });
1251
1324
  }
1252
1325
  }
1253
- catch {
1254
- // Ignore polling errors
1255
- }
1326
+ catch { /* ignore */ }
1256
1327
  }, 5000);
1257
1328
  }
1258
- /**
1259
- * Update connection status display
1260
- */
1261
1329
  function updateStatus(online) {
1262
- if (elements.statusDot) {
1330
+ if (elements.statusDot)
1263
1331
  elements.statusDot.classList.toggle('offline', !online);
1264
- }
1265
- if (elements.statusText) {
1332
+ if (elements.statusText)
1266
1333
  elements.statusText.textContent = online ? 'Online' : 'Offline';
1267
- }
1268
1334
  }
1269
1335
  // ============================================================================
1270
1336
  // PUBLIC API METHODS
1271
1337
  // ============================================================================
1272
- /**
1273
- * Open the chat widget window
1274
- */
1275
1338
  function open() {
1276
1339
  if (state.isOpen)
1277
1340
  return;
1278
1341
  state.isOpen = true;
1279
1342
  state.unreadCount = 0;
1280
- if (elements.badge) {
1343
+ if (elements.badge)
1281
1344
  elements.badge.textContent = '';
1282
- }
1283
- if (elements.toggle) {
1345
+ if (elements.toggle)
1284
1346
  elements.toggle.classList.add('open');
1285
- }
1286
- if (elements.window) {
1347
+ if (elements.window)
1287
1348
  elements.window.classList.add('open');
1288
- }
1349
+ hideProactiveToast();
1289
1350
  setTimeout(() => elements.input?.focus(), 300);
1290
1351
  emit('open');
1291
1352
  }
1292
- /**
1293
- * Close the chat widget window
1294
- */
1295
1353
  function close() {
1296
1354
  if (!state.isOpen)
1297
1355
  return;
1298
1356
  state.isOpen = false;
1299
- if (elements.toggle) {
1357
+ if (elements.toggle)
1300
1358
  elements.toggle.classList.remove('open');
1301
- }
1302
- if (elements.window) {
1359
+ if (elements.window)
1303
1360
  elements.window.classList.remove('open');
1361
+ // Check if we should show survey after closing (if conversation had enough messages)
1362
+ if (config.surveyConfig && state.messages.length >= 4) {
1363
+ const surveyCompleted = state.sessionId && storage('survey_completed_' + state.sessionId);
1364
+ if (!surveyCompleted) {
1365
+ // Show survey after a short delay
1366
+ setTimeout(() => {
1367
+ if (!state.isOpen) {
1368
+ checkAndShowSurvey();
1369
+ open(); // Re-open to show survey
1370
+ }
1371
+ }, 500);
1372
+ }
1304
1373
  }
1305
1374
  emit('close');
1306
1375
  }
1307
- /**
1308
- * Toggle the chat widget window
1309
- */
1310
1376
  function toggle() {
1311
1377
  state.isOpen ? close() : open();
1312
1378
  }
1313
- /**
1314
- * Start a new conversation
1315
- */
1379
+ function isOpenFn() {
1380
+ return state.isOpen;
1381
+ }
1316
1382
  function startNewConversation() {
1317
- // Clear session and visitor to force a completely new conversation
1318
1383
  state.sessionId = null;
1319
1384
  state.visitorId = null;
1320
1385
  state.messages = [];
1321
1386
  isLiveAgentMode = false;
1322
- // Clear stored session
1323
1387
  storage('session_id', null);
1324
- // Generate new visitor ID
1325
1388
  state.visitorId = generateId('v_');
1326
1389
  storage('visitor_id', state.visitorId);
1327
- // Clear UI
1328
- if (elements.messages) {
1390
+ if (elements.messages)
1329
1391
  elements.messages.innerHTML = '';
1330
- }
1331
- // Hide status bar
1332
1392
  updateStatusBar('hidden');
1333
- // Stop any live agent polling
1334
1393
  stopLiveAgentPolling();
1335
- // Reset reconnect attempts for fresh connection
1336
1394
  reconnectAttempts = 0;
1337
- // Close existing WebSocket with intentional flag to prevent auto-reconnect
1338
1395
  if (ws) {
1339
1396
  intentionalDisconnect = true;
1340
1397
  ws.close();
1341
1398
  ws = null;
1342
1399
  }
1343
- // Connect with new visitor ID
1344
1400
  connectWebSocket();
1345
- // Show welcome message
1346
- if (config.welcomeMessage) {
1401
+ if (config.welcomeMessage)
1347
1402
  addMessage(config.welcomeMessage, 'bot');
1348
- }
1403
+ // Show preset questions again
1404
+ if (elements.presetQuestions)
1405
+ elements.presetQuestions.classList.remove('hidden');
1406
+ renderPresetQuestions();
1349
1407
  emit('newConversation');
1350
1408
  }
1351
- /**
1352
- * Destroy the widget and clean up resources
1353
- */
1354
1409
  function destroy() {
1355
- // Stop heartbeat
1356
1410
  stopHeartbeat();
1357
- // Stop live agent polling
1358
1411
  stopLiveAgentPolling();
1359
- // Close WebSocket (intentionally, don't reconnect)
1412
+ cleanupProactiveMessages();
1360
1413
  intentionalDisconnect = true;
1361
1414
  if (ws) {
1362
1415
  ws.close();
1363
1416
  ws = null;
1364
1417
  }
1365
- // Clear polling interval
1366
1418
  if (pollInterval) {
1367
1419
  clearInterval(pollInterval);
1368
1420
  pollInterval = null;
1369
1421
  }
1370
- // Remove DOM elements
1371
- if (elements.widget) {
1422
+ if (elements.widget)
1372
1423
  elements.widget.remove();
1373
- }
1374
1424
  const styles = document.getElementById('ihooman-widget-styles');
1375
- if (styles) {
1425
+ if (styles)
1376
1426
  styles.remove();
1377
- }
1378
- // Reset state
1379
1427
  state = {
1380
1428
  isOpen: false,
1381
1429
  isConnected: false,
@@ -1388,21 +1436,15 @@
1388
1436
  reconnectAttempts = 0;
1389
1437
  intentionalDisconnect = false;
1390
1438
  }
1391
- /**
1392
- * Send a message programmatically
1393
- */
1394
1439
  function sendMessage(content) {
1395
1440
  if (!content.trim())
1396
1441
  return;
1397
- if (elements.input) {
1442
+ if (elements.input)
1398
1443
  elements.input.value = content;
1399
- }
1400
1444
  handleSendClick();
1401
1445
  }
1402
- /**
1403
- * Set user information
1404
- */
1405
1446
  function setUser(user) {
1447
+ state.userInfo = user;
1406
1448
  if (user.name)
1407
1449
  storage('user_name', user.name);
1408
1450
  if (user.email)
@@ -1410,79 +1452,53 @@
1410
1452
  if (user.metadata)
1411
1453
  storage('user_metadata', user.metadata);
1412
1454
  }
1413
- /**
1414
- * Clear chat history
1415
- */
1455
+ function clearUser() {
1456
+ state.userInfo = null;
1457
+ storage('user_name', null);
1458
+ storage('user_email', null);
1459
+ storage('user_metadata', null);
1460
+ }
1416
1461
  function clearHistory() {
1417
1462
  startNewConversation();
1418
1463
  }
1419
- /**
1420
- * Subscribe to widget events
1421
- */
1422
1464
  function on(event, callback) {
1423
- if (!eventListeners[event]) {
1465
+ if (!eventListeners[event])
1424
1466
  eventListeners[event] = [];
1425
- }
1426
1467
  eventListeners[event].push(callback);
1427
1468
  }
1428
- /**
1429
- * Unsubscribe from widget events
1430
- */
1431
1469
  function off(event, callback) {
1432
1470
  if (eventListeners[event]) {
1433
1471
  eventListeners[event] = eventListeners[event].filter((fn) => fn !== callback);
1434
1472
  }
1435
1473
  }
1436
- /**
1437
- * Get current widget state
1438
- */
1439
1474
  function getState() {
1440
1475
  return { ...state };
1441
1476
  }
1477
+ function getConfig() {
1478
+ return { ...config };
1479
+ }
1442
1480
  // ============================================================================
1443
1481
  // INITIALIZATION
1444
1482
  // ============================================================================
1445
- /**
1446
- * Fetch widget configuration from the Widget Configuration API
1447
- *
1448
- * Requirements:
1449
- * - 10.1: Widget only requires Widget ID, never API key
1450
- * - 10.2: Widget fetches configuration using only Widget ID
1451
- */
1452
1483
  async function fetchWidgetConfig(widgetId, serverUrl) {
1453
1484
  try {
1454
1485
  const response = await fetch(`${serverUrl}/api/widget/config?widget_id=${encodeURIComponent(widgetId)}`);
1455
1486
  if (!response.ok) {
1456
1487
  const errorData = await response.json().catch(() => ({}));
1457
- return {
1458
- success: false,
1459
- error: errorData.error || `HTTP ${response.status}`,
1460
- };
1488
+ return { success: false, error: errorData.error || `HTTP ${response.status}` };
1461
1489
  }
1462
1490
  return await response.json();
1463
1491
  }
1464
1492
  catch (error) {
1465
- return {
1466
- success: false,
1467
- error: 'Unable to connect. Please check your internet connection.',
1468
- };
1493
+ return { success: false, error: 'Unable to connect. Please check your internet connection.' };
1469
1494
  }
1470
1495
  }
1471
- /**
1472
- * Preset questions from server config
1473
- */
1474
- let presetQuestions = [];
1475
- /**
1476
- * Apply server configuration to local config
1477
- */
1478
1496
  function applyServerConfig(serverConfig) {
1479
- // Apply basic settings
1480
1497
  config.title = serverConfig.title || config.title;
1481
1498
  config.subtitle = serverConfig.subtitle || config.subtitle;
1482
1499
  config.welcomeMessage = serverConfig.welcomeMessage || config.welcomeMessage;
1483
1500
  config.placeholder = serverConfig.placeholder || config.placeholder;
1484
1501
  config.position = serverConfig.position || config.position;
1485
- // Apply theme settings
1486
1502
  if (serverConfig.theme) {
1487
1503
  config.primaryColor = serverConfig.theme.primaryColor || config.primaryColor;
1488
1504
  config.gradientFrom = serverConfig.theme.gradientFrom || config.gradientFrom;
@@ -1492,7 +1508,6 @@
1492
1508
  config.borderRadius = parseInt(serverConfig.theme.borderRadius, 10) || config.borderRadius;
1493
1509
  }
1494
1510
  }
1495
- // Apply behavior settings
1496
1511
  if (serverConfig.behavior) {
1497
1512
  config.startOpen = serverConfig.behavior.startOpen ?? config.startOpen;
1498
1513
  config.showTypingIndicator = serverConfig.behavior.showTypingIndicator ?? config.showTypingIndicator;
@@ -1501,72 +1516,91 @@
1501
1516
  config.enableFileUpload = serverConfig.behavior.enableFileUpload ?? config.enableFileUpload;
1502
1517
  config.persistSession = serverConfig.behavior.persistSession ?? config.persistSession;
1503
1518
  }
1504
- // Apply branding settings
1505
1519
  if (serverConfig.branding) {
1506
1520
  config.avatarUrl = serverConfig.branding.avatarUrl || config.avatarUrl;
1521
+ config.logoUrl = serverConfig.branding.logoUrl || config.logoUrl;
1507
1522
  config.poweredBy = serverConfig.branding.poweredBy ?? config.poweredBy;
1523
+ config.customCss = serverConfig.branding.customCss || config.customCss;
1524
+ }
1525
+ if (serverConfig.size) {
1526
+ config.width = serverConfig.size.width || config.width;
1527
+ config.height = serverConfig.size.height || config.height;
1528
+ config.buttonSize = serverConfig.size.buttonSize || config.buttonSize;
1508
1529
  }
1509
- // Store preset questions
1510
1530
  if (serverConfig.presetQuestions && Array.isArray(serverConfig.presetQuestions)) {
1511
- presetQuestions = serverConfig.presetQuestions;
1531
+ console.debug('[IhoomanChat] Server config presetQuestions:', serverConfig.presetQuestions.length);
1532
+ presetQuestions = serverConfig.presetQuestions.map(q => ({
1533
+ id: q.id,
1534
+ text: q.text,
1535
+ icon: q.icon,
1536
+ emoji: q.icon,
1537
+ }));
1538
+ config.presetQuestions = presetQuestions;
1539
+ }
1540
+ if (serverConfig.proactiveMessages && Array.isArray(serverConfig.proactiveMessages)) {
1541
+ console.debug('[IhoomanChat] Server config proactiveMessages:', serverConfig.proactiveMessages.length);
1542
+ config.proactiveMessages = serverConfig.proactiveMessages
1543
+ .filter(pm => pm.isActive !== false)
1544
+ .map(pm => ({
1545
+ id: pm.id,
1546
+ message: pm.message,
1547
+ trigger: {
1548
+ type: pm.trigger.type,
1549
+ value: pm.trigger.type === 'time' || pm.trigger.type === 'scroll'
1550
+ ? parseInt(pm.trigger.value, 10) || 30
1551
+ : pm.trigger.value,
1552
+ },
1553
+ autoOpen: pm.autoOpen || false,
1554
+ cooldownMinutes: pm.cooldownMinutes || 60,
1555
+ }));
1556
+ }
1557
+ if (serverConfig.surveyConfig && serverConfig.surveyConfig.isActive !== false) {
1558
+ console.debug('[IhoomanChat] Server config surveyConfig:', serverConfig.surveyConfig.id);
1559
+ config.surveyConfig = {
1560
+ id: serverConfig.surveyConfig.id,
1561
+ type: serverConfig.surveyConfig.type,
1562
+ question: serverConfig.surveyConfig.question,
1563
+ followUpQuestion: serverConfig.surveyConfig.followUpQuestion,
1564
+ thankYouMessage: serverConfig.surveyConfig.thankYouMessage,
1565
+ };
1512
1566
  }
1513
1567
  }
1514
- /**
1515
- * Initialize the widget
1516
- *
1517
- * Requirements:
1518
- * - 5.2: Export IhoomanChat object with init, open, close, toggle, destroy methods
1519
- * - 5.3: Accept widgetId configuration option for initialization
1520
- * - 10.1: Widget only requires Widget ID, never API key
1521
- * - 10.2: Widget fetches configuration using only Widget ID
1522
- */
1523
1568
  async function init(userConfig) {
1524
- // Validate required widgetId
1525
1569
  if (!userConfig.widgetId) {
1526
1570
  console.error('IhoomanChat: widgetId is required');
1527
1571
  return null;
1528
1572
  }
1529
- // Merge user config with defaults
1530
1573
  config = { ...defaultConfig, ...userConfig };
1531
- // Initialize visitor ID
1532
1574
  state.visitorId = storage('visitor_id') || generateId('v_');
1533
1575
  storage('visitor_id', state.visitorId);
1534
- // Restore session if persistence is enabled
1535
1576
  if (config.persistSession) {
1536
1577
  state.sessionId = storage('session_id');
1537
1578
  }
1538
- // Fetch configuration from Widget Configuration API using Widget ID
1539
- // This is the secure approach - no API key is ever sent from the client
1540
- const serverUrl = config.serverUrl || 'https://api.ihooman.ai';
1579
+ // Store page load time for proactive messages
1580
+ if (!storage('page_load_time')) {
1581
+ storage('page_load_time', Date.now());
1582
+ }
1583
+ const serverUrl = config.serverUrl || DEFAULT_SERVER_URL;
1541
1584
  const configResponse = await fetchWidgetConfig(config.widgetId, serverUrl);
1542
1585
  let chatEndpoint;
1543
1586
  if (configResponse.success && configResponse.config) {
1544
- // Apply server configuration
1545
1587
  applyServerConfig(configResponse.config);
1546
1588
  chatEndpoint = configResponse.chatEndpoint;
1547
1589
  }
1548
1590
  else if (configResponse.error) {
1549
- // Log error but continue with local config
1550
1591
  console.warn('IhoomanChat: Could not fetch server config:', configResponse.error);
1551
- // If domain validation failed, show error to user
1552
1592
  if (configResponse.error === 'Widget not authorized for this domain') {
1553
1593
  console.error('IhoomanChat: Widget configuration error. Please contact support.');
1554
- // Still create widget but show error state
1555
1594
  }
1556
1595
  }
1557
- // Create the widget DOM
1558
1596
  createWidget();
1559
- // Render preset questions (after config is loaded and widget is created)
1560
1597
  renderPresetQuestions();
1561
- // Restore messages from state (if any)
1562
- state.messages.forEach((msg) => addMessage(msg.content, msg.sender, msg.metadata));
1563
- // Show welcome message if no messages
1598
+ state.messages.forEach((msg) => addMessage(msg.content, msg.sender === 'user' ? 'user' : 'bot', msg.metadata));
1564
1599
  if (state.messages.length === 0 && config.welcomeMessage) {
1565
1600
  addMessage(config.welcomeMessage, 'bot');
1566
1601
  }
1567
- // Connect WebSocket for real-time messaging
1568
1602
  connectWebSocket(chatEndpoint);
1569
- // Auto-open if configured
1603
+ initializeProactiveMessages();
1570
1604
  if (config.startOpen) {
1571
1605
  setTimeout(open, 500);
1572
1606
  }
@@ -1576,43 +1610,30 @@
1576
1610
  // ============================================================================
1577
1611
  // PUBLIC API OBJECT
1578
1612
  // ============================================================================
1579
- /**
1580
- * Public API object
1581
- *
1582
- * Requirements:
1583
- * - 5.2: Export IhoomanChat object with init, open, close, toggle, destroy methods
1584
- */
1585
1613
  const publicAPI = {
1586
1614
  init,
1587
1615
  open,
1588
1616
  close,
1589
1617
  toggle,
1618
+ isOpen: isOpenFn,
1590
1619
  destroy,
1591
1620
  sendMessage,
1592
1621
  setUser,
1622
+ clearUser,
1593
1623
  clearHistory,
1594
1624
  on,
1595
1625
  off,
1596
1626
  getState,
1627
+ getConfig,
1597
1628
  version: VERSION,
1598
1629
  };
1599
- /**
1600
- * Main IhoomanChat export
1601
- */
1602
1630
  const IhoomanChat = publicAPI;
1603
- /**
1604
- * Factory function to create a new widget instance
1605
- */
1606
1631
  function createWidgetInstance() {
1607
1632
  return { ...publicAPI };
1608
1633
  }
1609
1634
  // ============================================================================
1610
1635
  // AUTO-INITIALIZATION
1611
1636
  // ============================================================================
1612
- /**
1613
- * Auto-initialize from script attributes
1614
- * Supports data-widget-id attribute for easy embedding
1615
- */
1616
1637
  (function autoInit() {
1617
1638
  if (typeof document === 'undefined')
1618
1639
  return;
@@ -1631,7 +1652,6 @@
1631
1652
  const widgetId = script.getAttribute('data-widget-id');
1632
1653
  if (widgetId) {
1633
1654
  const autoConfig = { widgetId };
1634
- // Parse optional attributes
1635
1655
  const serverUrl = script.getAttribute('data-server-url');
1636
1656
  if (serverUrl)
1637
1657
  autoConfig.serverUrl = serverUrl;
@@ -1644,7 +1664,6 @@
1644
1664
  const startOpen = script.getAttribute('data-start-open');
1645
1665
  if (startOpen === 'true')
1646
1666
  autoConfig.startOpen = true;
1647
- // Initialize when DOM is ready
1648
1667
  if (document.readyState === 'loading') {
1649
1668
  document.addEventListener('DOMContentLoaded', () => init(autoConfig));
1650
1669
  }
@@ -1653,7 +1672,6 @@
1653
1672
  }
1654
1673
  }
1655
1674
  })();
1656
- // Export for UMD builds
1657
1675
  if (typeof window !== 'undefined') {
1658
1676
  window.IhoomanChat = IhoomanChat;
1659
1677
  }