@ihoomanai/chat-widget 3.0.0 → 3.0.1

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