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