4 min read 3 wks ago

Notification Icons and Sounds

You can use the AI Engine client-side API to trigger events in the user’s browser when a reply is received. Which means this allows you to build a quick notification system. Here we will use the ai.reply filter, which triggers when a response has been sent in the chatbot, to listen for when a new reply has been made. We will show an icon so the user is aware a new response has been generated if they were not looking at the chatbot during this time.

You can try this on the chatbot in the bottom right corner just here: send a message, close the chatbot, and wait until you see a notification icon pop up.

Here is a really quick example of how you can replicate this behavior. This is just to get your started, feel free to customize this however you want of course.

MwaiAPI.addFilter('ai.reply', function (reply, args) {
  // Add notification dot after reply finishes
  const iconContainer = document.querySelector('.mwai-icon-container');

  if (iconContainer && !iconContainer.querySelector('.mwai-notification-dot')) {
    const dot = document.createElement('span');
    dot.classList.add('mwai-notification-dot');
    dot.style.cssText = `
      display: inline-block;
      width: 10px;
      height: 10px;
      background-color: red;
      border-radius: 50%;
      position: absolute;
      top: 0;
      right: 0;
    `;
    iconContainer.style.position = 'relative';
    iconContainer.appendChild(dot);
  }

  return reply; // don’t modify reply text
});

// Observe .mwai-chat for class changes
const chatEl = document.querySelector('.mwai-chat');
if (chatEl) {
  const observer = new MutationObserver(() => {
    if (chatEl.classList.contains('mwai-open')) {
      // Chat opened = message seen → remove notification dot
      const dot = document.querySelector('.mwai-notification-dot');
      if (dot) dot.remove();
    }
  });

  observer.observe(chatEl, { attributes: true, attributeFilter: ['class'] });
}
Here is the complete version used on this very page.
// 1) Hook: run when a bot reply finishes (timing only; we don't modify the text)
MwaiAPI.addFilter('ai.reply', function (reply, args) {
  ensureNotificationDot();
  return reply;
});

// 2) Observer: remove the dot when the chat opens (message seen)
setupChatOpenObserver();

/* ----------------- helpers ----------------- */

function ensureNotificationDot() {
  waitForEl('.mwai-icon-container', (iconContainer) => {
    // only one dot at a time
    if (iconContainer.querySelector('.mwai-notification-svg')) return;

    // make sure container can host an absolutely positioned child
    if (getComputedStyle(iconContainer).position === 'static') {
      iconContainer.style.position = 'relative';
    }

    injectNotificationStylesOnce();

    // cute, animated SVG: solid dot + "ping" ring
    const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    svg.setAttribute('class', 'mwai-notification-svg');
    svg.setAttribute('viewBox', '0 0 24 24');
    svg.setAttribute('width', '16');
    svg.setAttribute('height', '16');
    svg.setAttribute('aria-hidden', 'true');
    svg.setAttribute('role', 'img');

    // subtle glow
    const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
    const filter = document.createElementNS('http://www.w3.org/2000/svg', 'filter');
    filter.setAttribute('id', 'mwai-dot-glow');
    filter.innerHTML = `
      <feGaussianBlur in="SourceGraphic" stdDeviation="1.2" result="blur"/>
      <feMerge>
        <feMergeNode in="blur"/>
        <feMergeNode in="SourceGraphic"/>
      </feMerge>
    `;
    defs.appendChild(filter);
    svg.appendChild(defs);

    // solid center dot
    const dot = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
    dot.setAttribute('class', 'mwai-dot');
    dot.setAttribute('cx', '12');
    dot.setAttribute('cy', '12');
    dot.setAttribute('r', '5');
    dot.setAttribute('filter', 'url(#mwai-dot-glow)');
    svg.appendChild(dot);

    // pulsing ring
    const ping = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
    ping.setAttribute('class', 'mwai-ping');
    ping.setAttribute('cx', '12');
    ping.setAttribute('cy', '12');
    ping.setAttribute('r', '6');
    svg.appendChild(ping);

    // position in the corner
    svg.style.cssText = `
      position: absolute;
      top: -2px;
      right: -2px;
      pointer-events: none;
    `;

    iconContainer.appendChild(svg);
  });
}

function setupChatOpenObserver() {
  waitForEl('.mwai-chat', (chatEl) => {
    // clear immediately if it's already open
    if (chatEl.classList.contains('mwai-open')) removeNotificationDot();

    const observer = new MutationObserver(() => {
      if (chatEl.classList.contains('mwai-open')) {
        removeNotificationDot();
      }
    });

    observer.observe(chatEl, { attributes: true, attributeFilter: ['class'] });
  });
}

function removeNotificationDot() {
  const svg = document.querySelector('.mwai-notification-svg');
  if (!svg) return;

  // graceful fade-out
  svg.classList.add('mwai-dot-out');
  svg.addEventListener('animationend', () => svg.remove(), { once: true });
}

function injectNotificationStylesOnce() {
  if (document.getElementById('mwai-notification-styles')) return;

  const style = document.createElement('style');
  style.id = 'mwai-notification-styles';
  style.textContent = `
    .mwai-notification-svg .mwai-dot {
      fill: #ff3b30; /* iOS-style red */
    }
    .mwai-notification-svg .mwai-ping {
      fill: #ff3b30;
      opacity: 0.45;
      transform-origin: 12px 12px;
      animation: mwaiPing 1.4s ease-out infinite;
    }
    .mwai-notification-svg {
      animation: mwaiDotIn 160ms ease-out both;
    }
    .mwai-notification-svg.mwai-dot-out {
      animation: mwaiDotOut 180ms ease-in forwards;
    }

    @keyframes mwaiPing {
      0%   { transform: scale(0.9); opacity: 0.45; }
      70%  { transform: scale(1.8); opacity: 0.08; }
      100% { transform: scale(2.0); opacity: 0; }
    }
    @keyframes mwaiDotIn {
      0%   { transform: scale(0.7); opacity: 0; }
      100% { transform: scale(1); opacity: 1; }
    }
    @keyframes mwaiDotOut {
      0%   { transform: scale(1); opacity: 1; }
      100% { transform: scale(0.8); opacity: 0; }
    }

    /* motion-safe: keep it polite for users who prefer reduced motion */
    @media (prefers-reduced-motion: reduce) {
      .mwai-notification-svg .mwai-ping { animation: none; opacity: 0.25; }
      .mwai-notification-svg { animation: none; }
      .mwai-notification-svg.mwai-dot-out { animation: none; opacity: 0; }
    }
  `;
  document.head.appendChild(style);
}

function waitForEl(selector, cb, timeout = 8000) {
  const el = document.querySelector(selector);
  if (el) return cb(el);

  const obs = new MutationObserver(() => {
    const found = document.querySelector(selector);
    if (found) {
      obs.disconnect();
      cb(found);
    }
  });
  obs.observe(document.documentElement, { childList: true, subtree: true });

  // stop trying after a while
  setTimeout(() => obs.disconnect(), timeout);
}