Topcostura Norsec

Assistente virtual da lavandaria

Responde às perguntas do site com base na base de conhecimento editável. Pede o código postal para confirmar a zona de recolha, recolhe nome, e-mail e telemóvel e encaminha o cliente para o WhatsApp +351 912 343 340. Todas as conversas ficam gravadas.

Instalar no WordPress

Cole este código no rodapé do site (Aparência → Editor de tema → footer.php, ou um plugin de scripts). O botão fica no canto inferior direito, empilhado por cima do botão de WhatsApp (ajuste BUTTON_BOTTOM no topo do snippet se necessário). O snippet já escuta os eventos do assistente e envia-os para o dataLayer do GTM: chatbot_open, chatbot_first_message, chatbot_lead_shown, chatbot_lead_submitted e chatbot_out_of_area.

<script>
  (function () {
    var ORIGIN = 'https://ai-bot.topcostura-norsec.com';

    /* ===== AJUSTES RÁPIDOS ===== */
    /* Altura a que o nosso botão começa, para ficar ACIMA do botão de WhatsApp do site.
       Assume WhatsApp com ~60px a partir de bottom:20px. Ajuste conforme o seu site. */
    var BUTTON_BOTTOM = '92px';
    /* Em telemóvel sobe mais, para não colidir com barras de navegação fixas. */
    var BUTTON_BOTTOM_MOBILE = '112px';
    /* Distância à direita (mesma coluna do botão de WhatsApp). */
    var SIDE_OFFSET = '20px';
    /* Avatar do assistente (servido pelo CDN do assistente). */
    var AVATAR_PATH = '/__l5e/assets-v1/d1815672-af64-4265-b212-5addae5e02af/assistente.png';
    /* Cores — cores oficiais Topcostura.
       O ciano do logótipo é #00AEEF, mas com ícone branco por cima dá só 2,5:1.
       Por isso o GRADIENTE do botão começa em #0094CC (3,4:1) e acaba em #0077A8
       (5,0:1): o ícone branco fica sempre acima do mínimo de 3:1 exigido para
       elementos gráficos. O ciano puro fica no anel a pulsar e no foco, onde não
       tem texto por cima. */
    var BUTTON_GRAD_FROM = '#0094CC';
    var BUTTON_GRAD_TO = '#0077A8';
    var BUTTON_FG = '#ffffff';
    var RING_COLOR = '#00AEEF';
    var BADGE_BG = '#D92D20';
    var BADGE_FG = '#ffffff';
    /* Balão de convite */
    var INVITE_TEXT = 'Precisa de ajuda? Fale connosco 👋';
    var INVITE_DELAY = 4000;
    /* =========================== */

    window.dataLayer = window.dataLayer || [];

    var css = document.createElement('style');
    css.textContent =
      '@keyframes tcn-ring{0%{transform:scale(1);opacity:.55}70%{transform:scale(1.6);opacity:0}100%{transform:scale(1.6);opacity:0}}' +
      '@keyframes tcn-pop{0%{transform:scale(.6);opacity:0}60%{transform:scale(1.08);opacity:1}100%{transform:scale(1);opacity:1}}' +
      '@keyframes tcn-bubble{0%{transform:translateY(6px) scale(.96);opacity:0}100%{transform:translateY(0) scale(1);opacity:1}}' +
      '@keyframes tcn-breathe{0%,100%{transform:scale(1) rotate(0)}40%{transform:scale(1.06) rotate(-3deg)}70%{transform:scale(1.02) rotate(2deg)}}' +
      '@keyframes tcn-online{0%,100%{box-shadow:0 0 0 0 rgba(34,197,94,.6)}70%{box-shadow:0 0 0 6px rgba(34,197,94,0)}}' +
      '#tcn-chat-frame{position:fixed;right:' + SIDE_OFFSET + ';bottom:calc(' + BUTTON_BOTTOM + ' + 64px + env(safe-area-inset-bottom));width:380px;height:560px;max-height:80vh;border:0;border-radius:16px;box-shadow:0 12px 40px rgba(0,0,0,.18);z-index:99998;display:none;background:transparent}' +
      '#tcn-chat-button{position:fixed;right:' + SIDE_OFFSET + ';bottom:calc(' + BUTTON_BOTTOM + ' + env(safe-area-inset-bottom));width:56px;height:56px;padding:0;border:0;border-radius:50%;background-image:linear-gradient(140deg,' + BUTTON_GRAD_FROM + ' 0%,' + BUTTON_GRAD_TO + ' 100%);color:' + BUTTON_FG + ';font-size:24px;line-height:1;cursor:pointer;z-index:99999;box-shadow:0 10px 30px rgba(0,0,0,.32);transition:transform .2s ease,box-shadow .2s ease;animation:tcn-pop .42s cubic-bezier(.34,1.56,.64,1) both}' +
      '#tcn-chat-button img{width:56px;height:56px;border-radius:50%;display:block;animation:tcn-breathe 3.6s ease-in-out infinite}' +
      '#tcn-chat-dot{position:fixed;right:calc(' + SIDE_OFFSET + ' - 1px);bottom:calc(' + BUTTON_BOTTOM + ' + env(safe-area-inset-bottom));width:14px;height:14px;border-radius:50%;background:#22c55e;border:2px solid #fff;z-index:100000;pointer-events:none;animation:tcn-online 2.4s ease-out infinite}' +
      '#tcn-chat-button:hover{transform:scale(1.1);box-shadow:0 14px 38px rgba(0,0,0,.42)}' +
      '#tcn-chat-button:focus-visible{outline:3px solid ' + RING_COLOR + ';outline-offset:3px}' +
      '#tcn-chat-ring{position:fixed;right:' + SIDE_OFFSET + ';bottom:calc(' + BUTTON_BOTTOM + ' + env(safe-area-inset-bottom));width:56px;height:56px;border-radius:50%;background:' + RING_COLOR + ';z-index:99997;pointer-events:none;animation:tcn-ring 2.8s cubic-bezier(0,0,.2,1) infinite}' +
      '#tcn-chat-badge{position:fixed;right:calc(' + SIDE_OFFSET + ' - 2px);bottom:calc(' + BUTTON_BOTTOM + ' + 36px + env(safe-area-inset-bottom));min-width:20px;height:20px;border-radius:10px;background:' + BADGE_BG + ';color:' + BADGE_FG + ';font:700 11px/20px system-ui,sans-serif;text-align:center;z-index:100000;pointer-events:none;box-shadow:0 2px 6px rgba(0,0,0,.3)}' +
      '#tcn-chat-pill{position:fixed;right:calc(' + SIDE_OFFSET + ' + 56px + 12px);bottom:calc(' + BUTTON_BOTTOM + ' + env(safe-area-inset-bottom));z-index:99998;display:flex;align-items:center;gap:8px;padding:6px 12px;border-radius:999px;background:#fff;color:#111;border:1px solid #e5e7eb;box-shadow:0 12px 32px rgba(0,0,0,.22);font:400 12px/1.35 system-ui,sans-serif;transition:opacity .2s ease}' +
      '#tcn-chat-pill .tcn-x{margin-left:2px;display:flex;align-items:center;justify-content:center;width:18px;height:18px;border:0;border-radius:999px;background:transparent;color:#666;font-size:13px;cursor:pointer;transition:background .15s ease,color .15s ease}' +
      '#tcn-chat-pill .tcn-x:hover{background:#f3f4f6;color:#111}' +
      '#tcn-chat-pill .tcn-x:focus-visible{outline:2px solid ' + RING_COLOR + ';outline-offset:1px}' +
      '.tcn-pill-dot{width:10px;height:10px;border-radius:50%;background:#22c55e;animation:tcn-online 2.4s ease-out infinite}' +
      '.tcn-pill-full{display:inline}.tcn-pill-short{display:none}' +
      '#tcn-chat-invite{position:fixed;right:' + SIDE_OFFSET + ';bottom:calc(' + BUTTON_BOTTOM + ' + 68px + env(safe-area-inset-bottom));max-width:min(280px,calc(100vw - 32px));display:none;align-items:center;gap:8px;padding:10px 8px 10px 12px;border-radius:16px;background:#fff;color:#111;font:400 14px/1.35 system-ui,sans-serif;box-shadow:0 12px 32px rgba(0,0,0,.22);z-index:99999;animation:tcn-bubble .26s ease-out both}' +
      '#tcn-chat-invite button{border:0;background:none;font:inherit;color:inherit;cursor:pointer;padding:0}' +
      '#tcn-chat-invite .tcn-x{padding:2px 6px;border-radius:999px;color:#666;font-size:16px;line-height:1}' +
      '@media (max-width:640px){' +
      '#tcn-chat-frame{left:8px;right:8px;width:auto;height:75vh;height:75dvh;max-height:none;bottom:0;border-radius:16px 16px 0 0;padding-bottom:env(safe-area-inset-bottom)}' +
      '#tcn-chat-button,#tcn-chat-ring,#tcn-chat-dot{bottom:calc(' + BUTTON_BOTTOM_MOBILE + ' + env(safe-area-inset-bottom))}' +
      '#tcn-chat-badge{bottom:calc(' + BUTTON_BOTTOM_MOBILE + ' + 36px + env(safe-area-inset-bottom))}' +
      '#tcn-chat-pill{bottom:calc(' + BUTTON_BOTTOM_MOBILE + ' + env(safe-area-inset-bottom))}' +
      '#tcn-chat-invite{left:12px;right:12px;max-width:none;bottom:calc(' + BUTTON_BOTTOM_MOBILE + ' + 68px + env(safe-area-inset-bottom))}' +
      '}' +
      '@media (max-width:360px){.tcn-pill-full{display:none}.tcn-pill-short{display:inline}}' +
      '@media (prefers-reduced-motion:reduce){' +
      '#tcn-chat-button,#tcn-chat-invite,#tcn-chat-button img,#tcn-chat-dot,#tcn-chat-pill,.tcn-pill-dot{animation:none!important}' +
      '#tcn-chat-ring{animation:none!important;opacity:.35;transform:scale(1.25)}' +
      '#tcn-chat-button:hover{transform:none}' +
      '}';
    document.head.appendChild(css);

    var f = document.createElement('iframe');
    f.id = 'tcn-chat-frame';
    f.src = ORIGIN + '/embed';
    f.title = 'Assistente Topcostura Norsec';

    var ring = document.createElement('span');
    ring.id = 'tcn-chat-ring';
    ring.setAttribute('aria-hidden', 'true');

    var badge = document.createElement('span');
    badge.id = 'tcn-chat-badge';
    badge.setAttribute('aria-hidden', 'true');
    badge.textContent = '1';

    var invite = document.createElement('div');
    invite.id = 'tcn-chat-invite';
    invite.setAttribute('role', 'status');
    var inviteText = document.createElement('button');
    inviteText.type = 'button';
    inviteText.style.flex = '1';
    inviteText.style.textAlign = 'left';
    inviteText.textContent = INVITE_TEXT;
    var inviteClose = document.createElement('button');
    inviteClose.type = 'button';
    inviteClose.className = 'tcn-x';
    inviteClose.setAttribute('aria-label', 'Fechar convite');
    inviteClose.innerHTML = '&#10005;';
    invite.appendChild(inviteText);
    invite.appendChild(inviteClose);

    var INVITE_KEY = 'tcn-invite-dismissed';
    function hideInvite() {
      invite.style.display = 'none';
      try { sessionStorage.setItem(INVITE_KEY, '1'); } catch (err) {}
    }
    inviteClose.onclick = hideInvite;

    var b = document.createElement('button');
    b.id = 'tcn-chat-button';
    b.setAttribute('aria-label', 'Abrir assistente');
    var avatar = document.createElement('img');
    avatar.src = ORIGIN + AVATAR_PATH;
    avatar.alt = '';
    b.appendChild(avatar);

    var dot = document.createElement('span');
    dot.id = 'tcn-chat-dot';
    dot.setAttribute('aria-hidden', 'true');

    var pill = document.createElement('div');
    pill.id = 'tcn-chat-pill';
    pill.setAttribute('role', 'status');
    pill.setAttribute('aria-live', 'polite');
    pill.innerHTML = '<span class="tcn-pill-dot" aria-hidden="true"></span><span class="tcn-pill-full">Sofia online · Disponível para ajudar</span><span class="tcn-pill-short">Sofia online · Ajuda</span><button type="button" class="tcn-x" aria-label="Ocultar indicação de disponibilidade">&#10005;</button>';

    var pillDismissed = false;
    var PILL_KEY = 'tcn-pill-dismissed';
    function hidePill() {
      pillDismissed = true;
      pill.style.display = 'none';
      try { sessionStorage.setItem(PILL_KEY, '1'); } catch (err) {}
    }
    pill.querySelector('.tcn-x').onclick = hidePill;

    function toggle() {
      var visible = f.style.display === 'block';
      f.style.display = visible ? 'none' : 'block';
      pill.style.display = (visible && !pillDismissed) ? 'flex' : 'none';
      if (visible) {
        b.innerHTML = '';
        b.appendChild(avatar);
      } else {
        b.innerHTML = '&#10005;';
      }
      ring.style.display = visible ? '' : 'none';
      dot.style.display = visible ? '' : 'none';
      badge.style.display = 'none';
      hideInvite();
    }
    b.onclick = toggle;

    inviteText.onclick = function () { hideInvite(); if (f.style.display !== 'block') toggle(); };

    var ALLOWED = [
      'chatbot_open',
      'chatbot_first_message',
      'chatbot_lead_shown',
      'chatbot_lead_submitted',
      'chatbot_out_of_area'
    ];

    window.addEventListener('message', function (e) {
      if (e.origin !== ORIGIN) return;
      var d = e.data;
      if (!d || d.type !== 'topcostura-chatbot') return;
      if (ALLOWED.indexOf(d.event) === -1) return;
      window.dataLayer.push({ event: d.event, postal_code: d.postal_code });
    });

    document.body.appendChild(f);
    document.body.appendChild(ring);
    document.body.appendChild(pill);
    document.body.appendChild(b);
    document.body.appendChild(dot);
    document.body.appendChild(badge);
    document.body.appendChild(invite);

    pillDismissed = false;
    try { pillDismissed = sessionStorage.getItem(PILL_KEY) === '1'; } catch (err) {}
    if (pillDismissed) pill.style.display = 'none';

    var seen = false;
    try { seen = sessionStorage.getItem(INVITE_KEY) === '1'; } catch (err) {}
    if (!seen) {
      setTimeout(function () {
        if (f.style.display !== 'block') invite.style.display = 'flex';
      }, INVITE_DELAY);
    }
  })();
</script>

Base de conhecimento

O conhecimento do assistente está agora na base de dados e é editável no painel de administração, no separador “Base de conhecimento” — sem necessidade de novo deploy.

Zonas de recolha

Os códigos postais servidos estão em src/lib/postal-codes.ts (prefixos 4000 a 4599: Porto, Vila Nova de Gaia, Matosinhos, Maia e Gondomar). O ficheiro explica como alargar a cobertura.

Sofia online · Ajuda