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