@ihoomanai/chat-widget 2.5.14 → 3.0.1

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