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