@ihoomanai/chat-widget 2.2.0 → 2.2.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 CHANGED
@@ -1,8 +1,8 @@
1
1
  # SRI Hashes for @ihooman/chat-widget
2
2
 
3
- ## Version: 2.0.2
3
+ ## Version: 2.2.1
4
4
 
5
- Build Date: 2026-01-31T18:35:43.908Z
5
+ Build Date: 2026-02-01T13:15:35.563Z
6
6
 
7
7
  ## Subresource Integrity (SRI) Hashes
8
8
 
@@ -12,16 +12,16 @@ Use these hashes to ensure the integrity of the widget script when loading from
12
12
 
13
13
  | Version | URL | SRI Hash |
14
14
  |---------|-----|----------|
15
- | Major (v2) | `https://cdn.ihooman.ai/widget/v2/chat.min.js` | `sha384-BwEKqYlBOyZlghqPlT66iCF9MHI95yjpzfbgdeTbQvbEoeP3fC9+BD2L9TOSjIQ1` |
16
- | Full (v2.0.2) | `https://cdn.ihooman.ai/widget/v2.0.2/chat.min.js` | `sha384-BwEKqYlBOyZlghqPlT66iCF9MHI95yjpzfbgdeTbQvbEoeP3fC9+BD2L9TOSjIQ1` |
15
+ | Major (v2) | `https://cdn.ihooman.ai/widget/v2/chat.min.js` | `sha384-1TV7Gv8ewoNYzwRYWcYawDbU+vZ1q/cdsNPV1K8xrEf84hOpCzsgd76EhE5471rV` |
16
+ | Full (v2.2.1) | `https://cdn.ihooman.ai/widget/v2.2.1/chat.min.js` | `sha384-1TV7Gv8ewoNYzwRYWcYawDbU+vZ1q/cdsNPV1K8xrEf84hOpCzsgd76EhE5471rV` |
17
17
  | Latest | `https://cdn.ihooman.ai/widget/latest/chat.min.js` | *(SRI not recommended for latest)* |
18
18
 
19
19
  ### Unminified Bundle (For Development/Debugging)
20
20
 
21
21
  | Version | URL | SRI Hash |
22
22
  |---------|-----|----------|
23
- | Major (v2) | `https://cdn.ihooman.ai/widget/v2/chat.js` | `sha384-LYWKCqYwX9rPT78Wjc3aUadARCiUYxWz9LIHI3ctepwYK6Rzl6mCXUpSwI8+r9tX` |
24
- | Full (v2.0.2) | `https://cdn.ihooman.ai/widget/v2.0.2/chat.js` | `sha384-LYWKCqYwX9rPT78Wjc3aUadARCiUYxWz9LIHI3ctepwYK6Rzl6mCXUpSwI8+r9tX` |
23
+ | Major (v2) | `https://cdn.ihooman.ai/widget/v2/chat.js` | `sha384-ATTcz+Ml80Gdw0rEz9R+yB5mcTtya6wbjCeyqvmKDeN15oarj3+VE63tQ7GNh6Vr` |
24
+ | Full (v2.2.1) | `https://cdn.ihooman.ai/widget/v2.2.1/chat.js` | `sha384-ATTcz+Ml80Gdw0rEz9R+yB5mcTtya6wbjCeyqvmKDeN15oarj3+VE63tQ7GNh6Vr` |
25
25
  | Latest | `https://cdn.ihooman.ai/widget/latest/chat.js` | *(SRI not recommended for latest)* |
26
26
 
27
27
  ## Usage Examples
@@ -29,8 +29,8 @@ Use these hashes to ensure the integrity of the widget script when loading from
29
29
  ### With SRI (Recommended for versioned URLs)
30
30
 
31
31
  ```html
32
- <script src="https://cdn.ihooman.ai/widget/v2.0.2/chat.min.js"
33
- integrity="sha384-BwEKqYlBOyZlghqPlT66iCF9MHI95yjpzfbgdeTbQvbEoeP3fC9+BD2L9TOSjIQ1"
32
+ <script src="https://cdn.ihooman.ai/widget/v2.2.1/chat.min.js"
33
+ integrity="sha384-1TV7Gv8ewoNYzwRYWcYawDbU+vZ1q/cdsNPV1K8xrEf84hOpCzsgd76EhE5471rV"
34
34
  crossorigin="anonymous"></script>
35
35
  <script>
36
36
  IhoomanChat.init({
package/cdn/health.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "status": "healthy",
3
3
  "service": "ihooman-chat-widget-cdn",
4
- "version": "2.0.2",
5
- "timestamp": "2026-01-31T18:35:43.908Z"
4
+ "version": "2.2.1",
5
+ "timestamp": "2026-02-01T13:15:35.563Z"
6
6
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @ihoomanai/chat-widget v2.0.2
2
+ * @ihoomanai/chat-widget v2.2.1
3
3
  * Universal chat support widget for any website - secure Widget ID based initialization
4
4
  *
5
5
  * @license MIT
@@ -69,6 +69,8 @@
69
69
  minimize: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="5" y1="12" x2="19" y2="12"></line></svg>`,
70
70
  refresh: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"></polyline><polyline points="1 20 1 14 7 14"></polyline><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path></svg>`,
71
71
  bot: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="10" rx="2"></rect><circle cx="12" cy="5" r="2"></circle><path d="M12 7v4"></path></svg>`,
72
+ agent: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle></svg>`,
73
+ ticket: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><line x1="10" y1="9" x2="8" y2="9"></line></svg>`,
72
74
  };
73
75
  /**
74
76
  * Internal widget state
@@ -82,6 +84,7 @@
82
84
  visitorId: null,
83
85
  unreadCount: 0,
84
86
  };
87
+ let isLiveAgentMode = false;
85
88
  let elements = {};
86
89
  const eventListeners = {};
87
90
  /**
@@ -260,6 +263,41 @@
260
263
  .ihooman-powered { text-align: center; padding: 8px; font-size: 11px; color: ${mutedColor}; background: ${bgColor}; }
261
264
  .ihooman-powered a { color: ${primaryColor}; text-decoration: none; }
262
265
  .ihooman-error { padding: 16px; text-align: center; color: #ef4444; background: ${bgColor}; }
266
+ .ihooman-escalation-actions { display: flex; gap: 8px; margin-top: 12px; flex-wrap: wrap; }
267
+ .ihooman-escalation-btn { display: inline-flex; align-items: center; gap: 6px; padding: 10px 16px; border-radius: 8px; border: none; cursor: pointer; font-family: inherit; font-size: 13px; font-weight: 500; transition: all 0.2s ease; }
268
+ .ihooman-escalation-btn svg { width: 16px; height: 16px; }
269
+ .ihooman-escalation-btn.primary { background: linear-gradient(135deg, ${gradientFrom}, ${gradientTo}); color: white; }
270
+ .ihooman-escalation-btn.primary:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(0, 174, 255, 0.3); }
271
+ .ihooman-escalation-btn.secondary { background: ${isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.05)'}; color: ${textColor}; border: 1px solid ${borderColor}; }
272
+ .ihooman-escalation-btn.secondary:hover { background: ${isDark ? 'rgba(255,255,255,0.15)' : 'rgba(0,0,0,0.08)'}; }
273
+ .ihooman-escalation-btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
274
+ .ihooman-status-bar { padding: 10px 16px; text-align: center; font-size: 13px; display: none; }
275
+ .ihooman-status-bar.show { display: block; }
276
+ .ihooman-status-bar.waiting { background: #fef3c7; color: #92400e; }
277
+ .ihooman-status-bar.connected { background: #dcfce7; color: #166534; }
278
+ .ihooman-chat-view { display: flex; flex-direction: column; flex: 1; overflow: hidden; }
279
+ .ihooman-chat-view.hidden { display: none; }
280
+ .ihooman-ticket-view { display: none; flex-direction: column; padding: 20px; gap: 16px; background: ${bgColor}; flex: 1; overflow-y: auto; }
281
+ .ihooman-ticket-view.show { display: flex; }
282
+ .ihooman-ticket-title { font-size: 18px; font-weight: 600; color: ${textColor}; margin: 0; display: flex; align-items: center; gap: 8px; }
283
+ .ihooman-ticket-subtitle { font-size: 13px; color: ${mutedColor}; margin: 0; }
284
+ .ihooman-ticket-input { padding: 12px 14px; border: 1px solid ${borderColor}; border-radius: 10px; font-size: 14px; font-family: inherit; background: ${inputBg}; color: ${textColor}; outline: none; transition: border-color 0.2s; }
285
+ .ihooman-ticket-input:focus { border-color: ${primaryColor}; }
286
+ .ihooman-ticket-input::placeholder { color: ${mutedColor}; }
287
+ .ihooman-ticket-textarea { min-height: 100px; resize: vertical; }
288
+ .ihooman-ticket-submit { padding: 14px; background: linear-gradient(135deg, ${gradientFrom}, ${gradientTo}); color: white; border: none; border-radius: 10px; font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.2s; }
289
+ .ihooman-ticket-submit:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(0, 174, 255, 0.3); }
290
+ .ihooman-ticket-submit:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
291
+ .ihooman-ticket-back { padding: 10px; background: transparent; color: ${mutedColor}; border: 1px solid ${borderColor}; border-radius: 10px; font-size: 13px; cursor: pointer; transition: all 0.2s; }
292
+ .ihooman-ticket-back:hover { background: ${isDark ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.03)'}; }
293
+ .ihooman-action-buttons { display: flex; gap: 8px; padding: 0 16px 12px; }
294
+ .ihooman-action-btn { flex: 1; padding: 10px; border: none; border-radius: 8px; font-size: 13px; font-weight: 500; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 6px; transition: all 0.2s; }
295
+ .ihooman-action-btn svg { width: 16px; height: 16px; }
296
+ .ihooman-action-btn.ticket { background: #6366f1; color: white; }
297
+ .ihooman-action-btn.ticket:hover { background: #4f46e5; }
298
+ .ihooman-action-btn.live { background: #10b981; color: white; }
299
+ .ihooman-action-btn.live:hover { background: #059669; }
300
+ .ihooman-action-btn:disabled { opacity: 0.5; cursor: not-allowed; }
263
301
  @media (max-width: 480px) { .ihooman-window { width: calc(100vw - 20px); height: calc(100vh - 100px); left: 10px; right: 10px; bottom: 80px; max-height: none; } .ihooman-toggle { ${positionRight ? 'right: 16px' : 'left: 16px'}; bottom: 16px; } }
264
302
  `;
265
303
  }
@@ -298,14 +336,41 @@
298
336
  <button class="ihooman-header-btn" data-action="minimize" title="Minimize">${icons.minimize}</button>
299
337
  </div>
300
338
  </div>
301
- <div class="ihooman-messages" role="log" aria-live="polite"></div>
302
- <div class="ihooman-input-area">
303
- <div class="ihooman-input-wrapper">
304
- ${config.enableFileUpload ? `<button class="ihooman-input-btn attach" title="Attach file">${icons.attach}</button><input type="file" class="ihooman-file-input">` : ''}
305
- <textarea class="ihooman-input" placeholder="${escapeHtml(config.placeholder || 'Type a message...')}" rows="1" aria-label="Message input"></textarea>
306
- <button class="ihooman-input-btn send" title="Send message" disabled>${icons.send}</button>
339
+
340
+ <!-- Chat View -->
341
+ <div class="ihooman-chat-view">
342
+ <div class="ihooman-status-bar"></div>
343
+ <div class="ihooman-messages" role="log" aria-live="polite"></div>
344
+ <div class="ihooman-action-buttons">
345
+ <button class="ihooman-action-btn ticket" data-action="show-ticket">
346
+ <svg viewBox="0 0 24 24" fill="currentColor"><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-5 14H7v-2h7v2zm3-4H7v-2h10v2zm0-4H7V7h10v2z"/></svg>
347
+ Submit Ticket
348
+ </button>
349
+ <button class="ihooman-action-btn live" data-action="live-agent">
350
+ <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>
351
+ Live Agent
352
+ </button>
307
353
  </div>
354
+ <div class="ihooman-input-area">
355
+ <div class="ihooman-input-wrapper">
356
+ ${config.enableFileUpload ? `<button class="ihooman-input-btn attach" title="Attach file">${icons.attach}</button><input type="file" class="ihooman-file-input">` : ''}
357
+ <textarea class="ihooman-input" placeholder="${escapeHtml(config.placeholder || 'Type a message...')}" rows="1" aria-label="Message input"></textarea>
358
+ <button class="ihooman-input-btn send" title="Send message" disabled>${icons.send}</button>
359
+ </div>
360
+ </div>
361
+ </div>
362
+
363
+ <!-- Ticket Form View -->
364
+ <div class="ihooman-ticket-view">
365
+ <h4 class="ihooman-ticket-title">📝 Submit a Ticket</h4>
366
+ <p class="ihooman-ticket-subtitle">We'll get back to you via email</p>
367
+ <input class="ihooman-ticket-input" id="ihooman-ticket-name" placeholder="Your name" required>
368
+ <input class="ihooman-ticket-input" id="ihooman-ticket-email" type="email" placeholder="Your email" required>
369
+ <textarea class="ihooman-ticket-input ihooman-ticket-textarea" id="ihooman-ticket-issue" placeholder="Describe your issue..."></textarea>
370
+ <button class="ihooman-ticket-submit" id="ihooman-ticket-submit">Submit Ticket</button>
371
+ <button class="ihooman-ticket-back" id="ihooman-ticket-back">← Back to Chat</button>
308
372
  </div>
373
+
309
374
  ${config.poweredBy ? `<div class="ihooman-powered">Powered by <a href="https://ihooman.ai" target="_blank" rel="noopener">Ihooman AI</a></div>` : ''}
310
375
  </div>
311
376
  </div>
@@ -317,6 +382,8 @@
317
382
  toggle: widget.querySelector('.ihooman-toggle'),
318
383
  badge: widget.querySelector('.ihooman-badge'),
319
384
  window: widget.querySelector('.ihooman-window'),
385
+ chatView: widget.querySelector('.ihooman-chat-view'),
386
+ ticketView: widget.querySelector('.ihooman-ticket-view'),
320
387
  messages: widget.querySelector('.ihooman-messages'),
321
388
  input: widget.querySelector('.ihooman-input'),
322
389
  sendBtn: widget.querySelector('.ihooman-input-btn.send'),
@@ -324,6 +391,12 @@
324
391
  fileInput: widget.querySelector('.ihooman-file-input'),
325
392
  statusDot: widget.querySelector('.ihooman-status-dot'),
326
393
  statusText: widget.querySelector('.ihooman-status-text'),
394
+ statusBar: widget.querySelector('.ihooman-status-bar'),
395
+ ticketName: widget.querySelector('#ihooman-ticket-name'),
396
+ ticketEmail: widget.querySelector('#ihooman-ticket-email'),
397
+ ticketIssue: widget.querySelector('#ihooman-ticket-issue'),
398
+ ticketSubmitBtn: widget.querySelector('#ihooman-ticket-submit'),
399
+ ticketBackBtn: widget.querySelector('#ihooman-ticket-back'),
327
400
  };
328
401
  // Set up event listeners
329
402
  setupEventListeners();
@@ -358,6 +431,16 @@
358
431
  }
359
432
  elements.widget?.querySelector('[data-action="refresh"]')?.addEventListener('click', startNewConversation);
360
433
  elements.widget?.querySelector('[data-action="minimize"]')?.addEventListener('click', close);
434
+ // Action buttons - Submit Ticket and Live Agent
435
+ elements.widget?.querySelector('[data-action="show-ticket"]')?.addEventListener('click', handleShowTicketForm);
436
+ elements.widget?.querySelector('[data-action="live-agent"]')?.addEventListener('click', handleRequestLiveAgent);
437
+ // Ticket form buttons
438
+ if (elements.ticketSubmitBtn) {
439
+ elements.ticketSubmitBtn.addEventListener('click', handleSubmitTicket);
440
+ }
441
+ if (elements.ticketBackBtn) {
442
+ elements.ticketBackBtn.addEventListener('click', () => showView('chat'));
443
+ }
361
444
  }
362
445
  // ============================================================================
363
446
  // MESSAGING
@@ -378,10 +461,39 @@
378
461
  return message;
379
462
  const el = document.createElement('div');
380
463
  el.className = `ihooman-message ${sender}`;
464
+ // Check if this message should show escalation buttons
465
+ const showEscalationButtons = sender === 'bot' && metadata?.escalation_offered === true;
466
+ let escalationButtonsHtml = '';
467
+ if (showEscalationButtons) {
468
+ escalationButtonsHtml = `
469
+ <div class="ihooman-escalation-actions">
470
+ <button class="ihooman-escalation-btn primary" data-action="live-agent">
471
+ ${icons.agent}
472
+ <span>Talk to Agent</span>
473
+ </button>
474
+ <button class="ihooman-escalation-btn secondary" data-action="create-ticket">
475
+ ${icons.ticket}
476
+ <span>Create Ticket</span>
477
+ </button>
478
+ </div>
479
+ `;
480
+ }
381
481
  el.innerHTML = `
382
482
  <div class="ihooman-message-content">${parseMarkdown(content)}</div>
483
+ ${escalationButtonsHtml}
383
484
  ${config.showTimestamps ? `<div class="ihooman-message-time">${formatTime(message.timestamp)}</div>` : ''}
384
485
  `;
486
+ // Add event listeners for escalation buttons
487
+ if (showEscalationButtons) {
488
+ const liveAgentBtn = el.querySelector('[data-action="live-agent"]');
489
+ const ticketBtn = el.querySelector('[data-action="create-ticket"]');
490
+ if (liveAgentBtn) {
491
+ liveAgentBtn.addEventListener('click', () => handleEscalationAction('live-agent'));
492
+ }
493
+ if (ticketBtn) {
494
+ ticketBtn.addEventListener('click', () => handleEscalationAction('create-ticket'));
495
+ }
496
+ }
385
497
  // Remove typing indicator if present
386
498
  const typing = elements.messages.querySelector('.ihooman-typing');
387
499
  if (typing)
@@ -400,6 +512,250 @@
400
512
  emit('message', message);
401
513
  return message;
402
514
  }
515
+ /**
516
+ * Handle escalation action button clicks
517
+ */
518
+ function handleEscalationAction(action) {
519
+ // Disable all escalation buttons to prevent double-clicks
520
+ const buttons = document.querySelectorAll('.ihooman-escalation-btn');
521
+ buttons.forEach(btn => btn.disabled = true);
522
+ if (action === 'live-agent') {
523
+ handleRequestLiveAgent();
524
+ }
525
+ else if (action === 'create-ticket') {
526
+ handleShowTicketForm();
527
+ }
528
+ }
529
+ /**
530
+ * Switch between chat and ticket views
531
+ */
532
+ function showView(view) {
533
+ if (elements.chatView) {
534
+ elements.chatView.classList.toggle('hidden', view !== 'chat');
535
+ }
536
+ if (elements.ticketView) {
537
+ elements.ticketView.classList.toggle('show', view === 'ticket');
538
+ }
539
+ }
540
+ /**
541
+ * Show the ticket form
542
+ */
543
+ function handleShowTicketForm() {
544
+ showView('ticket');
545
+ // Focus on name input
546
+ setTimeout(() => elements.ticketName?.focus(), 100);
547
+ }
548
+ /**
549
+ * Update the status bar display
550
+ */
551
+ function updateStatusBar(status, message) {
552
+ if (!elements.statusBar)
553
+ return;
554
+ if (status === 'hidden') {
555
+ elements.statusBar.classList.remove('show');
556
+ return;
557
+ }
558
+ elements.statusBar.classList.add('show');
559
+ elements.statusBar.classList.remove('waiting', 'connected');
560
+ elements.statusBar.classList.add(status);
561
+ if (message) {
562
+ elements.statusBar.textContent = message;
563
+ }
564
+ }
565
+ /**
566
+ * Submit a ticket via the API
567
+ */
568
+ async function handleSubmitTicket() {
569
+ const name = elements.ticketName?.value.trim();
570
+ const email = elements.ticketEmail?.value.trim();
571
+ const issue = elements.ticketIssue?.value.trim();
572
+ if (!name || !email) {
573
+ alert('Please fill in your name and email');
574
+ return;
575
+ }
576
+ if (elements.ticketSubmitBtn) {
577
+ elements.ticketSubmitBtn.disabled = true;
578
+ elements.ticketSubmitBtn.textContent = 'Submitting...';
579
+ }
580
+ try {
581
+ const response = await fetch(`${config.serverUrl}/api/v1/public/submit-ticket`, {
582
+ method: 'POST',
583
+ headers: { 'Content-Type': 'application/json' },
584
+ body: JSON.stringify({
585
+ session_id: state.sessionId,
586
+ user_name: name,
587
+ user_email: email,
588
+ issue: issue || 'No description provided',
589
+ escalation_type: 'ticket',
590
+ widget_id: config.widgetId,
591
+ }),
592
+ });
593
+ const data = await response.json();
594
+ // Clear form
595
+ if (elements.ticketName)
596
+ elements.ticketName.value = '';
597
+ if (elements.ticketEmail)
598
+ elements.ticketEmail.value = '';
599
+ if (elements.ticketIssue)
600
+ elements.ticketIssue.value = '';
601
+ // Switch back to chat view
602
+ showView('chat');
603
+ // Show success message
604
+ const ticketRef = data.ticket_id ? data.ticket_id.slice(0, 8) : 'submitted';
605
+ addMessage(`✅ Ticket submitted! We'll contact you at ${email}. Reference: #${ticketRef}`, 'bot', { is_system_message: true });
606
+ }
607
+ catch (error) {
608
+ console.error('Error submitting ticket:', error);
609
+ addMessage('❌ Error submitting ticket. Please try again.', 'bot', { is_system_message: true });
610
+ }
611
+ if (elements.ticketSubmitBtn) {
612
+ elements.ticketSubmitBtn.disabled = false;
613
+ elements.ticketSubmitBtn.textContent = 'Submit Ticket';
614
+ }
615
+ }
616
+ /**
617
+ * Request a live agent via the API
618
+ */
619
+ async function handleRequestLiveAgent() {
620
+ if (!state.sessionId) {
621
+ addMessage('Please send a message first to start a conversation.', 'bot', { is_system_message: true });
622
+ return;
623
+ }
624
+ // Disable live agent button
625
+ const liveBtn = elements.widget?.querySelector('[data-action="live-agent"]');
626
+ if (liveBtn) {
627
+ liveBtn.disabled = true;
628
+ 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...';
629
+ }
630
+ try {
631
+ const response = await fetch(`${config.serverUrl}/api/v1/public/live-agent`, {
632
+ method: 'POST',
633
+ headers: { 'Content-Type': 'application/json' },
634
+ body: JSON.stringify({
635
+ session_id: state.sessionId,
636
+ visitor_id: state.visitorId,
637
+ widget_id: config.widgetId,
638
+ }),
639
+ });
640
+ const data = await response.json();
641
+ isLiveAgentMode = true;
642
+ // Show status bar - always start with waiting since agent hasn't accepted yet
643
+ if (data.position_in_queue && data.position_in_queue > 1) {
644
+ updateStatusBar('waiting', `⏳ Waiting for agent (Position: #${data.position_in_queue})`);
645
+ }
646
+ else {
647
+ updateStatusBar('waiting', '⏳ Connecting to live support...');
648
+ }
649
+ // Hide action buttons when in live agent mode
650
+ const actionsDiv = elements.widget?.querySelector('.ihooman-action-buttons');
651
+ if (actionsDiv) {
652
+ actionsDiv.style.display = 'none';
653
+ }
654
+ // Start polling for agent messages
655
+ startLiveAgentPolling();
656
+ }
657
+ catch (error) {
658
+ console.error('Error requesting live agent:', error);
659
+ addMessage('❌ Unable to connect to live support. Please try again.', 'bot', { is_system_message: true });
660
+ }
661
+ // Re-enable button
662
+ if (liveBtn) {
663
+ liveBtn.disabled = false;
664
+ 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';
665
+ }
666
+ }
667
+ /**
668
+ * Live agent polling interval
669
+ */
670
+ let liveAgentPollInterval = null;
671
+ let lastMessageCount = 0;
672
+ /**
673
+ * Start polling for live agent messages
674
+ */
675
+ function startLiveAgentPolling() {
676
+ if (liveAgentPollInterval)
677
+ return;
678
+ liveAgentPollInterval = setInterval(async () => {
679
+ if (!state.sessionId || !isLiveAgentMode) {
680
+ stopLiveAgentPolling();
681
+ return;
682
+ }
683
+ try {
684
+ const response = await fetch(`${config.serverUrl}/api/v1/public/transcript/${state.sessionId}`, {
685
+ headers: { 'Content-Type': 'application/json' },
686
+ });
687
+ if (!response.ok)
688
+ return;
689
+ const data = await response.json();
690
+ const msgCount = data.messages?.length || 0;
691
+ // Check for new messages
692
+ if (msgCount > lastMessageCount) {
693
+ // Get only new messages
694
+ const newMessages = data.messages.slice(lastMessageCount);
695
+ newMessages.forEach((msg) => {
696
+ // Only add agent messages (user messages are already shown)
697
+ if (msg.sender_type === 'agent') {
698
+ addMessage(msg.content, 'bot', { agent_name: 'Agent' });
699
+ }
700
+ else if (msg.sender_type === 'ai' && msg.content.includes('connected to live support')) {
701
+ // System message about connection
702
+ addMessage(msg.content, 'bot', { is_system_message: true });
703
+ }
704
+ });
705
+ lastMessageCount = msgCount;
706
+ }
707
+ // Check if conversation was closed
708
+ if (data.status === 'closed') {
709
+ isLiveAgentMode = false;
710
+ stopLiveAgentPolling();
711
+ updateStatusBar('hidden');
712
+ addMessage('This conversation has been closed. Thank you for contacting us!', 'bot', { is_system_message: true });
713
+ // Show action buttons again
714
+ const actionsDiv = elements.widget?.querySelector('.ihooman-action-buttons');
715
+ if (actionsDiv) {
716
+ actionsDiv.style.display = 'flex';
717
+ }
718
+ }
719
+ // Update queue position and connection status
720
+ try {
721
+ const escResponse = await fetch(`${config.serverUrl}/api/v1/public/escalation-status/${state.sessionId}`);
722
+ if (escResponse.ok) {
723
+ const escData = await escResponse.json();
724
+ if (escData.escalated) {
725
+ if (escData.ticket_status === 'in_progress') {
726
+ // Agent has accepted - show connected status
727
+ updateStatusBar('connected', '🟢 Connected to live agent');
728
+ }
729
+ else if (escData.ticket_status === 'open') {
730
+ // Still waiting in queue
731
+ if (escData.position_in_queue && escData.position_in_queue > 1) {
732
+ updateStatusBar('waiting', `⏳ Waiting for agent (Position: #${escData.position_in_queue})`);
733
+ }
734
+ else {
735
+ updateStatusBar('waiting', '⏳ Waiting for agent...');
736
+ }
737
+ }
738
+ }
739
+ }
740
+ }
741
+ catch {
742
+ // Ignore escalation status errors
743
+ }
744
+ }
745
+ catch (error) {
746
+ console.error('Error polling for messages:', error);
747
+ }
748
+ }, 3000);
749
+ }
750
+ /**
751
+ * Stop live agent polling
752
+ */
753
+ function stopLiveAgentPolling() {
754
+ if (liveAgentPollInterval) {
755
+ clearInterval(liveAgentPollInterval);
756
+ liveAgentPollInterval = null;
757
+ }
758
+ }
403
759
  /**
404
760
  * Show typing indicator
405
761
  */
@@ -610,6 +966,8 @@
610
966
  addMessage(data.content, sender, {
611
967
  confidence: data.confidence,
612
968
  agent_name: data.agent_name,
969
+ escalation_offered: data.escalation_offered,
970
+ escalated: data.escalated,
613
971
  });
614
972
  }
615
973
  else if (data.type === 'typing') {
@@ -761,6 +1119,8 @@
761
1119
  function destroy() {
762
1120
  // Stop heartbeat
763
1121
  stopHeartbeat();
1122
+ // Stop live agent polling
1123
+ stopLiveAgentPolling();
764
1124
  // Close WebSocket
765
1125
  if (ws) {
766
1126
  ws.close();