@ihoomanai/chat-widget 3.0.14 → 3.0.16

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.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @ihoomanai/chat-widget v3.0.6
2
+ * @ihoomanai/chat-widget v3.0.15
3
3
  * Universal chat support widget for any website - secure Widget ID based initialization
4
4
  *
5
5
  * @license MIT
@@ -16,7 +16,7 @@
16
16
  * Enhanced with professional features
17
17
  * @version 3.0.0
18
18
  */
19
- const VERSION = '3.0.2';
19
+ const VERSION = '3.0.13';
20
20
  const STORAGE_PREFIX = 'ihooman_chat_';
21
21
  const DEFAULT_SERVER_URL = 'https://api.ihooman.ai';
22
22
  const defaultConfig = {
@@ -168,6 +168,29 @@
168
168
  }
169
169
  }
170
170
  }
171
+ /**
172
+ * Track an analytics event to the server
173
+ */
174
+ async function trackAnalyticsEvent(eventType, metadata) {
175
+ if (!config.widgetId || !config.serverUrl)
176
+ return;
177
+ try {
178
+ const url = `${config.serverUrl}/widget-analytics/track?widget_id=${encodeURIComponent(config.widgetId)}`;
179
+ await fetch(url, {
180
+ method: 'POST',
181
+ headers: { 'Content-Type': 'application/json' },
182
+ body: JSON.stringify({
183
+ event_type: eventType,
184
+ session_id: state.sessionId,
185
+ visitor_id: state.visitorId,
186
+ metadata: metadata || {},
187
+ }),
188
+ });
189
+ }
190
+ catch (e) {
191
+ console.debug('[IhoomanChat] Failed to track event:', eventType, e);
192
+ }
193
+ }
171
194
  function getCurrentScrollDepth() {
172
195
  const scrollTop = window.scrollY || document.documentElement.scrollTop;
173
196
  const scrollHeight = document.documentElement.scrollHeight - window.innerHeight;
@@ -198,9 +221,21 @@
198
221
  const positionRight = config.position?.includes('right') ?? true;
199
222
  const positionBottom = config.position?.includes('bottom') ?? true;
200
223
  return `
201
- .ihooman-widget * { box-sizing: border-box; margin: 0; padding: 0; }
224
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
225
+ :host { all: initial; }
202
226
  .ihooman-widget { font-family: ${fontFamily}; font-size: 14px; line-height: 1.5; color: ${textColor}; -webkit-font-smoothing: antialiased; }
203
- .ihooman-widget button:not(.ihooman-toggle) { all: unset; box-sizing: border-box; cursor: pointer; font-family: inherit; display: inline-flex; align-items: center; justify-content: center; }
227
+ button {
228
+ font-family: inherit;
229
+ font-size: 100%;
230
+ line-height: inherit;
231
+ color: inherit;
232
+ margin: 0;
233
+ padding: 0;
234
+ background: transparent;
235
+ border: none;
236
+ cursor: pointer;
237
+ outline: none;
238
+ }
204
239
  .ihooman-toggle { position: fixed !important; ${positionRight ? 'right: 20px' : 'left: 20px'}; ${positionBottom ? 'bottom: 20px' : 'top: 20px'}; width: ${buttonSize}px !important; height: ${buttonSize}px !important; border-radius: 50% !important; background: linear-gradient(135deg, ${gradientFrom}, ${gradientTo}) !important; border: none !important; cursor: pointer; z-index: ${zIndex}; display: flex !important; 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; }
205
240
  .ihooman-toggle:hover { transform: scale(1.08); box-shadow: 0 6px 28px rgba(0, 174, 255, 0.45); }
206
241
  .ihooman-toggle:active { transform: scale(0.95); }
@@ -267,12 +302,7 @@
267
302
  .ihooman-powered { text-align: center; padding: 8px; font-size: 11px; color: ${mutedColor}; background: ${bgColor}; }
268
303
  .ihooman-powered a { color: ${primaryColor}; text-decoration: none; }
269
304
  .ihooman-escalation-actions { display: flex; gap: 8px; margin-top: 10px; flex-wrap: wrap; }
270
- .ihooman-escalation-btn { display: inline-flex; align-items: center; justify-content: center; gap: 6px; padding: 8px 14px; border-radius: 6px; border: none; cursor: pointer; font-family: inherit; font-size: 12px; font-weight: 500; transition: all 0.2s ease; line-height: 1.4; }
271
305
  .ihooman-escalation-btn svg { width: 14px; height: 14px; flex-shrink: 0; }
272
- .ihooman-escalation-btn.primary { background: linear-gradient(135deg, ${gradientFrom}, ${gradientTo}); color: white; }
273
- .ihooman-escalation-btn.primary:hover { opacity: 0.9; }
274
- .ihooman-escalation-btn.secondary { background: ${isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.05)'}; color: ${textColor}; border: 1px solid ${borderColor}; }
275
- .ihooman-escalation-btn.secondary:hover { background: ${isDark ? 'rgba(255,255,255,0.15)' : 'rgba(0,0,0,0.08)'}; }
276
306
  .ihooman-status-bar { padding: 10px 16px; text-align: center; font-size: 13px; display: none; }
277
307
  .ihooman-status-bar.show { display: block; }
278
308
  .ihooman-status-bar.waiting { background: #fef3c7; color: #92400e; }
@@ -308,9 +338,7 @@
308
338
  .ihooman-history-empty { padding: 40px; text-align: center; color: ${mutedColor}; font-size: 14px; }
309
339
  .ihooman-preset-questions { padding: 10px 16px; display: flex; flex-wrap: wrap; gap: 6px; background: ${bgColor}; border-top: 1px solid ${borderColor}; }
310
340
  .ihooman-preset-questions:empty { display: none; }
311
- .ihooman-preset-questions.hidden { display: none !important; }
312
- .ihooman-preset-btn { display: inline-flex; align-items: center; justify-content: center; gap: 4px; padding: 6px 10px; border-radius: 6px; border: 1px solid ${borderColor}; background: ${isDark ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.02)'}; color: ${textColor}; font-size: 12px; font-weight: 500; cursor: pointer; transition: all 0.2s; white-space: nowrap; line-height: 1.4; }
313
- .ihooman-preset-btn:hover { background: ${isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.05)'}; border-color: ${primaryColor}; }
341
+ .ihooman-preset-questions.hidden { display: none; }
314
342
  .ihooman-proactive-toast { position: fixed; ${positionRight ? 'right: 20px' : 'left: 20px'}; ${positionBottom ? 'bottom: 90px' : 'top: 90px'}; max-width: 300px; padding: 16px; background: ${bgColor}; border-radius: 12px; box-shadow: 0 10px 40px rgba(0,0,0,0.15); z-index: ${(zIndex ?? 9999) - 2}; opacity: 0; visibility: hidden; transform: translateY(10px); transition: all 0.3s ease; border: 1px solid ${borderColor}; }
315
343
  .ihooman-proactive-toast.show { opacity: 1; visibility: visible; transform: translateY(0); }
316
344
  .ihooman-proactive-toast-content { font-size: 14px; color: ${textColor}; margin-bottom: 12px; }
@@ -333,9 +361,6 @@
333
361
  .ihooman-survey-skip { display: inline-flex; align-items: center; justify-content: center; padding: 8px 16px; background: transparent; color: ${mutedColor}; border: none; font-size: 12px; cursor: pointer; line-height: 1.4; }
334
362
  .ihooman-survey-skip:hover { color: ${textColor}; }
335
363
  .ihooman-feedback-btns { display: flex; gap: 6px; margin-top: 6px; }
336
- .ihooman-feedback-btn { display: inline-flex; align-items: center; justify-content: center; padding: 4px 6px; border: 1px solid ${borderColor}; border-radius: 4px; background: transparent; cursor: pointer; color: ${mutedColor}; transition: all 0.2s; }
337
- .ihooman-feedback-btn:hover { background: ${isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.05)'}; }
338
- .ihooman-feedback-btn.active { background: ${primaryColor}; color: white; border-color: ${primaryColor}; }
339
364
  .ihooman-feedback-btn svg { width: 12px; height: 12px; }
340
365
  .ihooman-carousel { display: flex; gap: 12px; overflow-x: auto; padding: 8px 0; scroll-snap-type: x mandatory; }
341
366
  .ihooman-carousel::-webkit-scrollbar { height: 4px; }
@@ -345,24 +370,30 @@
345
370
  .ihooman-carousel-card-title { font-size: 14px; font-weight: 600; color: ${textColor}; margin-bottom: 4px; }
346
371
  .ihooman-carousel-card-desc { font-size: 12px; color: ${mutedColor}; margin-bottom: 8px; }
347
372
  .ihooman-carousel-card-btns { display: flex; flex-direction: column; gap: 6px; }
348
- .ihooman-carousel-card-btn { display: flex; align-items: center; justify-content: center; padding: 6px 10px; border: 1px solid ${borderColor}; border-radius: 6px; background: transparent; color: ${textColor}; font-size: 11px; cursor: pointer; transition: all 0.2s; text-align: center; line-height: 1.4; }
349
- .ihooman-carousel-card-btn:hover { background: ${isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.05)'}; border-color: ${primaryColor}; }
350
373
  .ihooman-quick-replies { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
351
- .ihooman-quick-reply { display: inline-flex; align-items: center; justify-content: center; padding: 5px 10px; border: 1px solid ${primaryColor}; border-radius: 14px; background: transparent; color: ${primaryColor}; font-size: 11px; cursor: pointer; transition: all 0.2s; line-height: 1.4; }
352
- .ihooman-quick-reply:hover { background: ${primaryColor}; color: white; }
353
374
  @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; } }
354
375
  `;
355
376
  }
356
377
  // ============================================================================
357
378
  // DOM CREATION
358
379
  // ============================================================================
380
+ let shadowRoot = null;
359
381
  function createWidget() {
382
+ // Create host element
383
+ const host = document.createElement('div');
384
+ host.id = 'ihooman-chat-widget-host';
385
+ host.style.cssText = 'all: initial !important; position: fixed !important; z-index: 2147483647 !important; pointer-events: none !important;';
386
+ document.body.appendChild(host);
387
+ // Create Shadow DOM for style isolation
388
+ shadowRoot = host.attachShadow({ mode: 'open' });
389
+ // Add styles inside shadow DOM
360
390
  const styleEl = document.createElement('style');
361
391
  styleEl.id = 'ihooman-widget-styles';
362
392
  styleEl.textContent = generateStyles();
363
- document.head.appendChild(styleEl);
393
+ shadowRoot.appendChild(styleEl);
364
394
  const widget = document.createElement('div');
365
395
  widget.className = 'ihooman-widget';
396
+ widget.style.cssText = 'pointer-events: auto;';
366
397
  widget.innerHTML = `
367
398
  <button class="ihooman-toggle" aria-label="Open chat">
368
399
  <span class="ihooman-pulse"></span>
@@ -430,7 +461,7 @@
430
461
  </div>
431
462
  </div>
432
463
  `;
433
- document.body.appendChild(widget);
464
+ shadowRoot.appendChild(widget);
434
465
  elements = {
435
466
  widget,
436
467
  toggle: widget.querySelector('.ihooman-toggle'),
@@ -502,6 +533,11 @@
502
533
  const proactiveSecondaryBtn = elements.proactiveToast?.querySelector('.ihooman-proactive-toast-btn.secondary');
503
534
  if (proactivePrimaryBtn) {
504
535
  proactivePrimaryBtn.addEventListener('click', () => {
536
+ // Track click for the most recently shown proactive message
537
+ const lastShownId = shownProactiveIds[shownProactiveIds.length - 1];
538
+ if (lastShownId) {
539
+ trackAnalyticsEvent('proactive_clicked', { message_id: lastShownId });
540
+ }
505
541
  hideProactiveToast();
506
542
  open();
507
543
  emit('proactive:clicked');
@@ -883,7 +919,8 @@
883
919
  if (response.ok) {
884
920
  const data = await response.json();
885
921
  if (data.escalated) {
886
- if (data.ticket_status === 'in_progress') {
922
+ // Check both ticket_status and conversation_status for agent handling
923
+ if (data.ticket_status === 'in_progress' || data.conversation_status === 'active') {
887
924
  updateStatusBar('connected', '🟢 Connected to live agent');
888
925
  }
889
926
  else if (data.ticket_status === 'open') {
@@ -948,6 +985,14 @@
948
985
  comment,
949
986
  }),
950
987
  });
988
+ // Track survey submission analytics
989
+ trackAnalyticsEvent('survey_submitted', {
990
+ survey_id: config.surveyConfig?.id,
991
+ rating,
992
+ comment: comment || null,
993
+ session_id: state.sessionId,
994
+ visitor_id: state.visitorId,
995
+ });
951
996
  // Mark survey as completed for this session
952
997
  storage('survey_completed_' + state.sessionId, true);
953
998
  emit('survey:submitted', { rating, comment });
@@ -971,6 +1016,12 @@
971
1016
  // Don't show if not enough messages (at least 2 exchanges)
972
1017
  if (state.messages.length < 4)
973
1018
  return;
1019
+ // Track survey displayed analytics
1020
+ trackAnalyticsEvent('survey_displayed', {
1021
+ survey_id: config.surveyConfig.id,
1022
+ session_id: state.sessionId,
1023
+ visitor_id: state.visitorId,
1024
+ });
974
1025
  // Show the survey
975
1026
  showView('survey');
976
1027
  emit('survey:shown', config.surveyConfig);
@@ -983,6 +1034,13 @@
983
1034
  console.warn('[IhoomanChat] No survey configured');
984
1035
  return;
985
1036
  }
1037
+ // Track survey displayed analytics
1038
+ trackAnalyticsEvent('survey_displayed', {
1039
+ survey_id: config.surveyConfig.id,
1040
+ session_id: state.sessionId,
1041
+ visitor_id: state.visitorId,
1042
+ trigger: 'manual',
1043
+ });
986
1044
  showView('survey');
987
1045
  emit('survey:shown', config.surveyConfig);
988
1046
  }
@@ -1014,6 +1072,12 @@
1014
1072
  if (elements.sendBtn)
1015
1073
  elements.sendBtn.disabled = true;
1016
1074
  addMessage(content, 'user');
1075
+ // Track message sent analytics
1076
+ trackAnalyticsEvent('message_sent', {
1077
+ session_id: state.sessionId,
1078
+ visitor_id: state.visitorId,
1079
+ message_length: content.length,
1080
+ });
1017
1081
  showTyping();
1018
1082
  sendMessageToServer(content);
1019
1083
  }
@@ -1051,6 +1115,14 @@
1051
1115
  escalation_offered: data.escalation_offered,
1052
1116
  quick_replies: data.quick_replies,
1053
1117
  });
1118
+ // Track message received analytics
1119
+ trackAnalyticsEvent('message_received', {
1120
+ session_id: state.sessionId,
1121
+ visitor_id: state.visitorId,
1122
+ message_length: data.response.length,
1123
+ has_sources: !!data.sources,
1124
+ confidence: data.confidence,
1125
+ });
1054
1126
  }
1055
1127
  }
1056
1128
  catch (error) {
@@ -1084,13 +1156,38 @@
1084
1156
  });
1085
1157
  target.value = '';
1086
1158
  }
1159
+ // Notification sound using Web Audio API for reliable cross-browser playback
1087
1160
  function playSound() {
1161
+ if (state.soundMuted)
1162
+ return;
1088
1163
  try {
1089
- const audio = new Audio('data:audio/mp3;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU4Ljc2LjEwMAAAAAAAAAAAAAAA//tQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWGluZwAAAA8AAAACAAABhgC7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7//////////////////////////////////////////////////////////////////8AAAAATGF2YzU4LjEzAAAAAAAAAAAAAAAAJAAAAAAAAAAAAYYNBrP/AAAAAAAAAAAAAAAAAAAAAP/7UMQAA8AAAaQAAAAgAAA0gAAABExBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//tQxBKDwAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVU=');
1090
- audio.volume = 0.3;
1091
- audio.play().catch(() => { });
1164
+ // Use Web Audio API for more reliable sound generation
1165
+ const AudioContext = window.AudioContext || window.webkitAudioContext;
1166
+ if (!AudioContext)
1167
+ return;
1168
+ const audioCtx = new AudioContext();
1169
+ const oscillator = audioCtx.createOscillator();
1170
+ const gainNode = audioCtx.createGain();
1171
+ oscillator.connect(gainNode);
1172
+ gainNode.connect(audioCtx.destination);
1173
+ // Create a pleasant notification tone (two-tone chime)
1174
+ oscillator.frequency.setValueAtTime(880, audioCtx.currentTime); // A5
1175
+ oscillator.frequency.setValueAtTime(1108.73, audioCtx.currentTime + 0.1); // C#6
1176
+ // Envelope for smooth sound
1177
+ gainNode.gain.setValueAtTime(0, audioCtx.currentTime);
1178
+ gainNode.gain.linearRampToValueAtTime(0.3, audioCtx.currentTime + 0.01);
1179
+ gainNode.gain.linearRampToValueAtTime(0.2, audioCtx.currentTime + 0.1);
1180
+ gainNode.gain.linearRampToValueAtTime(0, audioCtx.currentTime + 0.3);
1181
+ oscillator.start(audioCtx.currentTime);
1182
+ oscillator.stop(audioCtx.currentTime + 0.3);
1183
+ // Clean up
1184
+ oscillator.onended = () => {
1185
+ audioCtx.close().catch(() => { });
1186
+ };
1187
+ }
1188
+ catch (e) {
1189
+ console.debug('[IhoomanChat] Sound playback failed:', e);
1092
1190
  }
1093
- catch { /* ignore */ }
1094
1191
  }
1095
1192
  // ============================================================================
1096
1193
  // PROACTIVE MESSAGES
@@ -1171,6 +1268,8 @@
1171
1268
  proactiveCooldowns[pm.id] = Date.now();
1172
1269
  storage('shown_proactive', shownProactiveIds);
1173
1270
  storage('proactive_cooldowns', proactiveCooldowns);
1271
+ // Track analytics event
1272
+ trackAnalyticsEvent('proactive_displayed', { message_id: pm.id });
1174
1273
  // Show toast
1175
1274
  if (elements.proactiveToast) {
1176
1275
  const content = elements.proactiveToast.querySelector('.ihooman-proactive-toast-content');
@@ -1285,12 +1384,35 @@
1285
1384
  else if (data.type === 'message') {
1286
1385
  hideTyping();
1287
1386
  const sender = data.sender === 'user' ? 'user' : 'bot';
1387
+ // Check if this is an agent-related system message
1388
+ const metadata = data.metadata || {};
1389
+ if (metadata.agent_accepted) {
1390
+ // Agent has accepted the escalation - update status
1391
+ isLiveAgentMode = true;
1392
+ updateStatusBar('connected', '🟢 Connected to live agent');
1393
+ emit('escalation:accepted', { type: 'live_agent' });
1394
+ }
1395
+ if (metadata.closed_by_agent) {
1396
+ // Agent has closed the conversation - return to AI mode
1397
+ isLiveAgentMode = false;
1398
+ stopLiveAgentPolling();
1399
+ updateStatusBar('hidden');
1400
+ emit('escalation:end', { type: 'live_agent' });
1401
+ }
1402
+ // Track message received analytics
1403
+ trackAnalyticsEvent('message_received', {
1404
+ session_id: state.sessionId,
1405
+ visitor_id: state.visitorId,
1406
+ sender: data.sender,
1407
+ is_system_message: metadata.is_system_message || false,
1408
+ });
1288
1409
  addMessage(data.content, sender, {
1289
1410
  confidence: data.confidence,
1290
1411
  agent_name: data.agent_name,
1291
1412
  escalation_offered: data.escalation_offered,
1292
1413
  escalated: data.escalated,
1293
1414
  quick_replies: data.quick_replies,
1415
+ ...metadata,
1294
1416
  });
1295
1417
  }
1296
1418
  else if (data.type === 'typing') {
@@ -1299,6 +1421,20 @@
1299
1421
  else if (data.type === 'pong') {
1300
1422
  // Heartbeat response
1301
1423
  }
1424
+ else if (data.type === 'agent_status') {
1425
+ // Handle agent status updates
1426
+ if (data.status === 'connected') {
1427
+ isLiveAgentMode = true;
1428
+ updateStatusBar('connected', '🟢 Connected to live agent');
1429
+ emit('escalation:accepted', { type: 'live_agent' });
1430
+ }
1431
+ else if (data.status === 'disconnected') {
1432
+ isLiveAgentMode = false;
1433
+ stopLiveAgentPolling();
1434
+ updateStatusBar('hidden');
1435
+ emit('escalation:end', { type: 'live_agent' });
1436
+ }
1437
+ }
1302
1438
  else if (data.type === 'error') {
1303
1439
  console.error('WebSocket server error:', data.message);
1304
1440
  emit('error', { message: data.message });
@@ -1370,6 +1506,8 @@
1370
1506
  elements.window.classList.add('open');
1371
1507
  hideProactiveToast();
1372
1508
  setTimeout(() => elements.input?.focus(), 300);
1509
+ // Track widget open event
1510
+ trackAnalyticsEvent('widget_open');
1373
1511
  emit('open');
1374
1512
  }
1375
1513
  function close() {
@@ -1380,6 +1518,8 @@
1380
1518
  elements.toggle.classList.remove('open');
1381
1519
  if (elements.window)
1382
1520
  elements.window.classList.remove('open');
1521
+ // Track widget close event
1522
+ trackAnalyticsEvent('widget_close');
1383
1523
  // Check if we should show survey after closing (if conversation had enough messages)
1384
1524
  if (config.surveyConfig && state.messages.length >= 4) {
1385
1525
  const surveyCompleted = state.sessionId && storage('survey_completed_' + state.sessionId);
@@ -1524,85 +1664,228 @@
1524
1664
  default: return 500;
1525
1665
  }
1526
1666
  }
1667
+ // Default button styles - MUST match client-dashboard/src/components/widget/ButtonStylingEditor.tsx
1668
+ const DEFAULT_BUTTON_STYLES = {
1669
+ primary: {
1670
+ backgroundColor: '#00aeff',
1671
+ textColor: '#ffffff',
1672
+ borderColor: 'transparent',
1673
+ borderWidth: 0,
1674
+ borderRadius: 8,
1675
+ paddingVertical: 8,
1676
+ paddingHorizontal: 16,
1677
+ fontSize: 13,
1678
+ fontWeight: 'medium',
1679
+ },
1680
+ secondary: {
1681
+ backgroundColor: 'transparent',
1682
+ textColor: '#6b7280',
1683
+ borderColor: '#e5e7eb',
1684
+ borderWidth: 1,
1685
+ borderRadius: 8,
1686
+ paddingVertical: 8,
1687
+ paddingHorizontal: 16,
1688
+ fontSize: 13,
1689
+ fontWeight: 'medium',
1690
+ },
1691
+ presetQuestions: {
1692
+ backgroundColor: 'rgba(0, 174, 255, 0.1)',
1693
+ textColor: '#00aeff',
1694
+ borderColor: 'rgba(0, 174, 255, 0.2)',
1695
+ borderWidth: 1,
1696
+ borderRadius: 20,
1697
+ paddingVertical: 6,
1698
+ paddingHorizontal: 12,
1699
+ fontSize: 12,
1700
+ fontWeight: 'medium',
1701
+ },
1702
+ quickReplies: {
1703
+ backgroundColor: '#f3f4f6',
1704
+ textColor: '#374151',
1705
+ borderColor: '#e5e7eb',
1706
+ borderWidth: 1,
1707
+ borderRadius: 16,
1708
+ paddingVertical: 5,
1709
+ paddingHorizontal: 10,
1710
+ fontSize: 11,
1711
+ fontWeight: 'medium',
1712
+ },
1713
+ feedback: {
1714
+ backgroundColor: 'transparent',
1715
+ textColor: '#9ca3af',
1716
+ borderColor: '#e5e7eb',
1717
+ borderWidth: 1,
1718
+ borderRadius: 6,
1719
+ paddingVertical: 4,
1720
+ paddingHorizontal: 8,
1721
+ fontSize: 12,
1722
+ fontWeight: 'medium',
1723
+ },
1724
+ headerActions: {
1725
+ backgroundColor: 'rgba(255, 255, 255, 0.1)',
1726
+ textColor: '#ffffff',
1727
+ borderColor: 'transparent',
1728
+ borderWidth: 0,
1729
+ borderRadius: 6,
1730
+ paddingVertical: 6,
1731
+ paddingHorizontal: 6,
1732
+ fontSize: 14,
1733
+ fontWeight: 'medium',
1734
+ },
1735
+ escalation: {
1736
+ backgroundColor: '#00aeff',
1737
+ textColor: '#ffffff',
1738
+ borderColor: 'transparent',
1739
+ borderWidth: 0,
1740
+ borderRadius: 8,
1741
+ paddingVertical: 8,
1742
+ paddingHorizontal: 14,
1743
+ fontSize: 12,
1744
+ fontWeight: 'medium',
1745
+ },
1746
+ cardActions: {
1747
+ backgroundColor: '#00aeff',
1748
+ textColor: '#ffffff',
1749
+ borderColor: 'transparent',
1750
+ borderWidth: 0,
1751
+ borderRadius: 6,
1752
+ paddingVertical: 6,
1753
+ paddingHorizontal: 12,
1754
+ fontSize: 12,
1755
+ fontWeight: 'medium',
1756
+ },
1757
+ toggleButton: {
1758
+ backgroundColor: '#00aeff',
1759
+ textColor: '#ffffff',
1760
+ borderColor: 'transparent',
1761
+ borderWidth: 0,
1762
+ borderRadius: 30,
1763
+ paddingVertical: 0,
1764
+ paddingHorizontal: 0,
1765
+ fontSize: 14,
1766
+ fontWeight: 'medium',
1767
+ },
1768
+ attachButton: {
1769
+ backgroundColor: 'transparent',
1770
+ textColor: '#6b7280',
1771
+ borderColor: 'transparent',
1772
+ borderWidth: 0,
1773
+ borderRadius: 8,
1774
+ paddingVertical: 6,
1775
+ paddingHorizontal: 6,
1776
+ fontSize: 14,
1777
+ fontWeight: 'medium',
1778
+ },
1779
+ };
1527
1780
  function applyButtonStyling(buttonStyling) {
1528
1781
  const styleId = 'ihooman-button-styling';
1529
- let styleEl = document.getElementById(styleId);
1782
+ // Get the shadow root or fall back to document.head
1783
+ const styleContainer = shadowRoot || document.head;
1784
+ let styleEl = styleContainer.querySelector(`#${styleId}`);
1530
1785
  if (!styleEl) {
1531
1786
  styleEl = document.createElement('style');
1532
1787
  styleEl.id = styleId;
1533
- document.head.appendChild(styleEl);
1534
- }
1788
+ styleContainer.appendChild(styleEl);
1789
+ }
1790
+ // Merge provided styles with defaults
1791
+ const mergedStyles = {
1792
+ primary: { ...DEFAULT_BUTTON_STYLES.primary, ...(buttonStyling?.primary || {}) },
1793
+ secondary: { ...DEFAULT_BUTTON_STYLES.secondary, ...(buttonStyling?.secondary || {}) },
1794
+ presetQuestions: { ...DEFAULT_BUTTON_STYLES.presetQuestions, ...(buttonStyling?.presetQuestions || {}) },
1795
+ quickReplies: { ...DEFAULT_BUTTON_STYLES.quickReplies, ...(buttonStyling?.quickReplies || {}) },
1796
+ feedback: { ...DEFAULT_BUTTON_STYLES.feedback, ...(buttonStyling?.feedback || {}) },
1797
+ headerActions: { ...DEFAULT_BUTTON_STYLES.headerActions, ...(buttonStyling?.headerActions || {}) },
1798
+ escalation: { ...DEFAULT_BUTTON_STYLES.escalation, ...(buttonStyling?.escalation || {}) },
1799
+ cardActions: { ...DEFAULT_BUTTON_STYLES.cardActions, ...(buttonStyling?.cardActions || {}) },
1800
+ toggleButton: { ...DEFAULT_BUTTON_STYLES.toggleButton, ...(buttonStyling?.toggleButton || {}) },
1801
+ attachButton: { ...DEFAULT_BUTTON_STYLES.attachButton, ...(buttonStyling?.attachButton || {}) },
1802
+ };
1535
1803
  const generateButtonCSS = (selector, style) => {
1536
- if (!style)
1537
- return '';
1538
- const bg = style.backgroundColor || '';
1539
- const color = style.textColor || '';
1540
- const borderColor = style.borderColor || 'transparent';
1541
- const borderWidth = style.borderWidth || 0;
1542
- const borderRadius = style.borderRadius || 8;
1543
- const paddingV = style.paddingVertical || 8;
1544
- const paddingH = style.paddingHorizontal || 16;
1545
- const fontSize = style.fontSize || 13;
1546
- const fontWeight = getFontWeightValue(style.fontWeight || 'medium');
1547
- // Use 'background' instead of 'background-color' to override gradients
1804
+ const bg = style.backgroundColor;
1805
+ const color = style.textColor;
1806
+ const borderColor = style.borderColor;
1807
+ const borderWidth = style.borderWidth;
1808
+ const borderRadius = style.borderRadius;
1809
+ const paddingV = style.paddingVertical;
1810
+ const paddingH = style.paddingHorizontal;
1811
+ const fontSize = style.fontSize;
1812
+ const fontWeight = getFontWeightValue(style.fontWeight);
1813
+ // Shadow DOM provides isolation, so we don't need !important or high specificity
1548
1814
  return `
1549
- .ihooman-widget ${selector} {
1550
- ${bg ? `background: ${bg} !important;` : ''}
1551
- ${color ? `color: ${color} !important;` : ''}
1552
- border: ${borderWidth}px solid ${borderColor} !important;
1553
- border-radius: ${borderRadius}px !important;
1554
- padding: ${paddingV}px ${paddingH}px !important;
1555
- font-size: ${fontSize}px !important;
1556
- font-weight: ${fontWeight} !important;
1557
- min-width: auto !important;
1558
- min-height: auto !important;
1559
- width: auto !important;
1560
- height: auto !important;
1561
- display: inline-flex !important;
1562
- align-items: center !important;
1563
- justify-content: center !important;
1815
+ ${selector} {
1816
+ background: ${bg};
1817
+ color: ${color};
1818
+ border: ${borderWidth}px solid ${borderColor};
1819
+ border-radius: ${borderRadius}px;
1820
+ padding: ${paddingV}px ${paddingH}px;
1821
+ margin: 0;
1822
+ font-family: inherit;
1823
+ font-size: ${fontSize}px;
1824
+ font-weight: ${fontWeight};
1825
+ min-width: unset;
1826
+ min-height: unset;
1827
+ width: auto;
1828
+ height: auto;
1829
+ max-width: none;
1830
+ max-height: none;
1831
+ display: inline-flex;
1832
+ align-items: center;
1833
+ justify-content: center;
1834
+ gap: 6px;
1835
+ line-height: 1.4;
1836
+ text-transform: none;
1837
+ letter-spacing: normal;
1838
+ box-shadow: none;
1839
+ text-decoration: none;
1840
+ white-space: nowrap;
1841
+ box-sizing: border-box;
1842
+ cursor: pointer;
1843
+ transition: all 0.2s ease;
1564
1844
  }
1565
1845
  `;
1566
1846
  };
1567
1847
  let css = '';
1568
1848
  // Primary buttons (send, submit, etc)
1569
- if (buttonStyling.primary) {
1570
- css += generateButtonCSS('.ihooman-input-btn.send', buttonStyling.primary);
1571
- css += generateButtonCSS('.ihooman-proactive-toast-btn.primary', buttonStyling.primary);
1572
- css += generateButtonCSS('.ihooman-survey-submit', buttonStyling.primary);
1573
- }
1849
+ css += generateButtonCSS('.ihooman-input-btn.send', mergedStyles.primary);
1850
+ css += generateButtonCSS('.ihooman-proactive-toast-btn.primary', mergedStyles.primary);
1851
+ css += generateButtonCSS('.ihooman-survey-submit', mergedStyles.primary);
1852
+ css += generateButtonCSS('.ihooman-ticket-submit', mergedStyles.primary);
1853
+ css += generateButtonCSS('.ihooman-history-new', mergedStyles.primary);
1574
1854
  // Secondary buttons
1575
- if (buttonStyling.secondary) {
1576
- css += generateButtonCSS('.ihooman-proactive-toast-btn.secondary', buttonStyling.secondary);
1577
- css += generateButtonCSS('.ihooman-survey-skip', buttonStyling.secondary);
1578
- css += generateButtonCSS('.ihooman-ticket-back', buttonStyling.secondary);
1579
- }
1855
+ css += generateButtonCSS('.ihooman-proactive-toast-btn.secondary', mergedStyles.secondary);
1856
+ css += generateButtonCSS('.ihooman-survey-skip', mergedStyles.secondary);
1857
+ css += generateButtonCSS('.ihooman-ticket-back', mergedStyles.secondary);
1580
1858
  // Preset question buttons
1581
- if (buttonStyling.presetQuestions) {
1582
- css += generateButtonCSS('.ihooman-preset-btn', buttonStyling.presetQuestions);
1583
- }
1859
+ css += generateButtonCSS('.ihooman-preset-btn', mergedStyles.presetQuestions);
1584
1860
  // Quick reply buttons
1585
- if (buttonStyling.quickReplies) {
1586
- css += generateButtonCSS('.ihooman-quick-reply', buttonStyling.quickReplies);
1587
- }
1861
+ css += generateButtonCSS('.ihooman-quick-reply', mergedStyles.quickReplies);
1588
1862
  // Feedback buttons (thumbs up/down)
1589
- if (buttonStyling.feedback) {
1590
- css += generateButtonCSS('.ihooman-feedback-btn', buttonStyling.feedback);
1591
- }
1863
+ css += generateButtonCSS('.ihooman-feedback-btn', mergedStyles.feedback);
1592
1864
  // Header action buttons
1593
- if (buttonStyling.headerActions) {
1594
- css += generateButtonCSS('.ihooman-header-btn', buttonStyling.headerActions);
1595
- }
1865
+ css += generateButtonCSS('.ihooman-header-btn', mergedStyles.headerActions);
1596
1866
  // Escalation buttons (Talk to Agent, Create Ticket)
1597
- if (buttonStyling.escalation) {
1598
- css += generateButtonCSS('.ihooman-escalation-btn', buttonStyling.escalation);
1599
- css += generateButtonCSS('.ihooman-escalation-btn.primary', buttonStyling.escalation);
1600
- css += generateButtonCSS('.ihooman-escalation-btn.secondary', buttonStyling.escalation);
1601
- }
1867
+ css += generateButtonCSS('.ihooman-escalation-btn', mergedStyles.escalation);
1868
+ css += generateButtonCSS('.ihooman-escalation-btn.primary', mergedStyles.escalation);
1869
+ css += generateButtonCSS('.ihooman-escalation-btn.secondary', mergedStyles.secondary);
1602
1870
  // Card action buttons
1603
- if (buttonStyling.cardActions) {
1604
- css += generateButtonCSS('.ihooman-carousel-card-btn', buttonStyling.cardActions);
1605
- }
1871
+ css += generateButtonCSS('.ihooman-carousel-card-btn', mergedStyles.cardActions);
1872
+ // Toggle button (floating chat bubble) - special handling for circular button
1873
+ const toggleStyle = mergedStyles.toggleButton;
1874
+ const toggleBg = toggleStyle.backgroundColor;
1875
+ const toggleColor = toggleStyle.textColor;
1876
+ const toggleBorderColor = toggleStyle.borderColor;
1877
+ const toggleBorderWidth = toggleStyle.borderWidth;
1878
+ const toggleBorderRadius = toggleStyle.borderRadius;
1879
+ css += `
1880
+ .ihooman-toggle {
1881
+ background: linear-gradient(135deg, ${toggleBg}, ${toggleBg}) !important;
1882
+ color: ${toggleColor};
1883
+ border: ${toggleBorderWidth}px solid ${toggleBorderColor};
1884
+ border-radius: ${toggleBorderRadius}px;
1885
+ }
1886
+ `;
1887
+ // Attach button (file attachment)
1888
+ css += generateButtonCSS('.ihooman-input-btn.attach', mergedStyles.attachButton);
1606
1889
  console.debug('[IhoomanChat] Applied button styling CSS:', css.length, 'chars');
1607
1890
  styleEl.textContent = css;
1608
1891
  }
@@ -1705,6 +1988,10 @@
1705
1988
  if (configResponse.success && configResponse.config) {
1706
1989
  applyServerConfig(configResponse.config);
1707
1990
  chatEndpoint = configResponse.chatEndpoint;
1991
+ // Update serverUrl with apiBaseUrl from server response
1992
+ if (configResponse.apiBaseUrl) {
1993
+ config.serverUrl = configResponse.apiBaseUrl;
1994
+ }
1708
1995
  }
1709
1996
  else if (configResponse.error) {
1710
1997
  console.warn('IhoomanChat: Could not fetch server config:', configResponse.error);
@@ -1713,6 +2000,7 @@
1713
2000
  }
1714
2001
  }
1715
2002
  createWidget();
2003
+ applyButtonStyling(config.buttonStyling);
1716
2004
  renderPresetQuestions();
1717
2005
  state.messages.forEach((msg) => addMessage(msg.content, msg.sender === 'user' ? 'user' : 'bot', msg.metadata));
1718
2006
  if (state.messages.length === 0 && config.welcomeMessage) {