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