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