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