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