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