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