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