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