Display notification bubble

    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Webchat Notification Bubble</title>
    
        <script src="https://cdn.botpress.cloud/webchat/v3.2/inject.js"></script>
        <script src="https://files.bpcontent.cloud/xxxx/xx/xx/xx/xxxxxxxxxxxxx-xxxxxxxx.js" defer></script>
        <script src="index.js" defer></script>
    
        <link rel="stylesheet" href="styles.css">
      </head>
      <body>
    
      <!-- Notification Bubble -->
      <div id="msgDiv" class="cb tri-right btm-right" role="button" tabindex="0" aria-live="polite" aria-label="Notification" style="display:none">
        <span id="msgText"></span>
        <button class="close-chip" type="button" aria-label="Dismiss">×</button>
      </div>
    
      </body>
    </html>
    function openNotification() {
        if (window.botpress) {
            window.botpress.open();
        } else {
            alert('Botpress integration not available.');
        }
    }
    
    (function () {
        const msgDiv = document.getElementById('msgDiv');
        const msgText = document.getElementById('msgText');
        const closeBtn = msgDiv?.querySelector('.close-chip');
    
        function showBubble(text) {
            if (!msgDiv || !msgText) return;
    
            msgText.textContent = text || '';
            msgDiv.style.display = 'block';
    
            msgDiv.onclick = () => openNotification();
    
            msgDiv.focus();
            msgDiv.onkeydown = (e) => {
                if (e.key === 'Enter' || e.key === ' ') {
                    e.preventDefault();
                    openNotification();
                }
            };
    
            window.botpress.on('webchat:opened', () => {
                if (msgDiv) msgDiv.style.display = 'none';
            });
    
            setTimeout(() => (msgDiv.style.display = 'none'), 15000);
        }
    
        if (closeBtn) {
            closeBtn.addEventListener('click', (e) => {
                e.stopPropagation();
                msgDiv.style.display = 'none';
            });
        }
    
        function extractPayload(raw) {
            let data = raw;
    
            if (typeof data === 'string') {
                try { data = JSON.parse(data); } catch { /* ignore */ }
            }
    
            if (data && typeof data.event === 'string') {
                try { data = JSON.parse(data.event); } catch { data = { eventType: data.eventType, ...data }; }
            } else if (data && typeof data.event === 'object') {
                data = data.event;
            }
    
            return data || {};
        }
    
        if (window.botpress?.on) {
            window.botpress.on('customEvent', (payload) => {
                const webchat = document.querySelector('iframe[title="Botpress"]');
                const data = extractPayload(payload);
    
                if (data?.eventType === 'notification' && webchat && webchat.classList.contains('bpClose')) {
                    showBubble(data.message);
                }
            });
        }
    })();
    :root {
      /* Change these to match your page's brand */
      --bubble-bg: #ccddfa;   /* background */
      --bubble-fg: #173569;   /* text */
    }
    
    .cb {
      position: fixed;
      right: 24px;
      bottom: 102px;
      z-index: 9999;
    
      display: none; /* shown by JS */
      background: var(--bubble-bg);
      color: var(--bubble-fg);
      border-radius: 16px;
      padding: 14px 42px 14px 16px;
      max-width: 340px;
      line-height: 1.35;
      box-shadow: 0 10px 25px rgba(0, 0, 0, .18);
      cursor: pointer;
    
      font-family: Roboto, system-ui, -apple-system, Segoe UI, Roboto, "Helvetica Neue", Arial;
      font-size: 15px;
      transition: transform .2s ease, box-shadow .2s ease, opacity .2s ease;
      animation: bubble-in .28s cubic-bezier(.2, .8, .2, 1) both;
    }
    
    .cb:hover {
      transform: translateY(-1px);
      box-shadow: 0 12px 30px rgba(0, 0, 0, .22);
    }
    
    .cb:focus {
      outline: none;
    }
    
    /* Chat tail */
    .tri-right.btm-right::after {
      content: "";
      position: absolute;
      right: 22px;
      bottom: -8px;
      width: 16px;
      height: 16px;
      background: var(--bubble-bg);
      transform: rotate(45deg);
      box-shadow: 0 10px 25px rgba(0, 0, 0, .18);
      border-bottom-right-radius: 4px;
    }
    
    /* Close chip (optional, inside the bubble) */
    .cb .close-chip {
      position: absolute;
      top: 8px;
      right: 8px;
      width: 26px;
      height: 26px;
      border-radius: 999px;
      background: rgba(255, 255, 255, .18);
      color: var(--bubble-fg);
      border: 0;
      cursor: pointer;
      line-height: 1;
      font-size: 16px;
    }
    
    .cb .close-chip:hover {
      background: rgba(255, 255, 255, .28);
    }
    
    @keyframes bubble-in {
      from { transform: translateY(10px) scale(.98); opacity: 0; }
      to   { transform: translateY(0)   scale(1);   opacity: 1; }
    }

    You can make your website display a notification bubble when your bot sends a new message.

    The notification bubble is only displayed if the Webchat window is closed when you receive the message. You can then:

    • Open the notification (by clicking it or pressing Enter / Space)
    • Dismiss the notification (by clicking ×)
    • Open Webchat using the FAB (which dismisses the notification automatically)

    Step 1: Add a Hook to your bot

    1. In the Studio, navigate to the Hooks section.
    2. Select Create Hook and set its type to Before Outgoing.
    3. Paste the following code into the Hook:
    await actions.webchat.customEvent({
      conversationId: event.conversationId,
      event: JSON.stringify({
        eventType: 'notification',
        message: outgoingEvent.preview,
      }),
    })

    This Hook notifies your website every time your bot sends a new message—that way, your website can display a notification bubble.

    Step 2: Configure your website

    Now, you can configure your website to display the notification bubble.

    Add the bubble HTML element

    First, add the bubble element somewhere in your website’s HTML:

    <!-- Notification Bubble -->
    <div
      id="msgDiv"
      class="cb tri-right btm-right"
      role="button"
      tabindex="0"
      aria-live="polite"
      aria-label="Notification"
      style="display:none"
    >
      <span id="msgText"></span>
      <button class="close-chip" type="button" aria-label="Dismiss">×</button>
    </div>

    Add JavaScript

    Then, add the following snippet to your website’s JavaScript:

    function openNotification() {
      if (window.botpress) {
        window.botpress.open()
      } else {
        alert('Botpress integration not available.')
      }
    }
    
    ;(function () {
      const msgDiv = document.getElementById('msgDiv')
      const msgText = document.getElementById('msgText')
      const closeBtn = msgDiv?.querySelector('.close-chip')
    
      function showBubble(text) {
        if (!msgDiv || !msgText) return
    
        // Display the notification bubble
        msgText.textContent = text || ''
        msgDiv.style.display = 'block'
    
        // Avoid stacking handlers on repeated events
        msgDiv.onclick = () => openNotification()
    
        // Keyboard access
        msgDiv.focus()
        msgDiv.onkeydown = (e) => {
          if (e.key === 'Enter' || e.key === ' ') {
            e.preventDefault()
            openNotification()
          }
        }
    
        // Hide the notification bubble if Webchat is opened
        window.botpress.on('webchat:opened', () => {
          if (msgDiv) msgDiv.style.display = 'none'
        })
    
        // Optional auto-hide after a period
        setTimeout(() => (msgDiv.style.display = 'none'), 15000)
      }
    
      // Close/dismiss without action
      if (closeBtn) {
        closeBtn.addEventListener('click', (e) => {
          e.stopPropagation()
          msgDiv.style.display = 'none'
        })
      }
    
      // Extract payload from event
      function extractPayload(raw) {
        let data = raw
    
        // If the data is a string
        if (typeof data === 'string') {
          try {
            data = JSON.parse(data)
          } catch {
            /* ignore */
          }
        }
    
        // Handle both strings and objects as event types
        if (data && typeof data.event === 'string') {
          try {
            data = JSON.parse(data.event)
          } catch {
            data = { eventType: data.eventType, ...data }
          }
        } else if (data && typeof data.event === 'object') {
          data = data.event
        }
    
        return data || {}
      }
    
      if (window.botpress?.on) {
        // Listen for custom events from the bot
        window.botpress.on('customEvent', (payload) => {
          const webchat = document.querySelector('iframe[title="Botpress"]')
          const data = extractPayload(payload)
    
          // Show the notification bubble if the Webchat window is closed
          if (data?.eventType === 'notification' && webchat && webchat.classList.contains('bpClose')) {
            showBubble(data.message)
          }
        })
      }
    })()

    This handles the behaviour of the notification bubble. For example:

    • Displays the message received from the event’s payload
    • Remains visible for 15 seconds before automatically closing
    • Can be selected by clicking or pressing Enter / Space

    Add CSS

    Finally, add this CSS snippet to your website’s stylesheet:

    :root {
      /* Change these to match your page's brand */
      --bubble-bg: #ccddfa; /* background */
      --bubble-fg: #173569; /* text */
    }
    
    .cb {
      position: fixed;
      right: 24px;
      bottom: 102px;
      z-index: 9999;
    
      display: none; /* shown by JS */
      background: var(--bubble-bg);
      color: var(--bubble-fg);
      border-radius: 16px;
      padding: 14px 42px 14px 16px;
      max-width: 340px;
      line-height: 1.35;
      box-shadow: 0 10px 25px rgba(0, 0, 0, 0.18);
      cursor: pointer;
    
      font-family:
        Roboto,
        system-ui,
        -apple-system,
        Segoe UI,
        Roboto,
        'Helvetica Neue',
        Arial;
      font-size: 15px;
      transition:
        transform 0.2s ease,
        box-shadow 0.2s ease,
        opacity 0.2s ease;
      animation: bubble-in 0.28s cubic-bezier(0.2, 0.8, 0.2, 1) both;
    }
    
    .cb:hover {
      transform: translateY(-1px);
      box-shadow: 0 12px 30px rgba(0, 0, 0, 0.22);
    }
    
    .cb:focus {
      outline: none;
    }
    
    /* Chat tail */
    .tri-right.btm-right::after {
      content: '';
      position: absolute;
      right: 22px;
      bottom: -8px;
      width: 16px;
      height: 16px;
      background: var(--bubble-bg);
      transform: rotate(45deg);
      box-shadow: 0 10px 25px rgba(0, 0, 0, 0.18);
      border-bottom-right-radius: 4px;
    }
    
    /* Close chip (optional, inside the bubble) */
    .cb .close-chip {
      position: absolute;
      top: 8px;
      right: 8px;
      width: 26px;
      height: 26px;
      border-radius: 999px;
      background: rgba(255, 255, 255, 0.18);
      color: var(--bubble-fg);
      border: 0;
      cursor: pointer;
      line-height: 1;
      font-size: 16px;
    }
    
    .cb .close-chip:hover {
      background: rgba(255, 255, 255, 0.28);
    }
    
    @keyframes bubble-in {
      from {
        transform: translateY(10px) scale(0.98);
        opacity: 0;
      }
      to {
        transform: translateY(0) scale(1);
        opacity: 1;
      }
    }

    You can modify the styling as needed.