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