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