Chat Widget — Documentação Completa de Implementação

Projeto: Suaid Global — suaidglobal.com Componente: Chat Widget de Qualificação de Leads Data: 2026-03-24 Status: Implementado no base.njk com flag useCustomWidget


1. ESTADO ATUAL DA IMPLEMENTAÇÃO

Arquivos criados

ArquivoLinhasFunção
src/css/chat-widget.css464Estilos do widget, variáveis --cw-*, responsivo, mobile
src/js/chat-widget.js748Engine do chat: flow, validações, mascaras, syncToGHL, tracking
src/_includes/partials/chat-widget.njk59HTML estrutural (toggle + window + input)
src/chat-widget/chat-widget.html~1350Demo standalone (CSS+JS+HTML inline, para testes)
src/chat-widget/suaid-form-proxy-worker.js~380Cloudflare Worker atualizado com progressive sync

Alteração no base.njk

O src/_includes/layouts/base.njk foi modificado com uma abordagem conservadora usando flag useCustomWidget:

Linha 63-67: CSS condicional no <head>
  
    <link rel="stylesheet" href="/css/chat-widget.css?v=..." ...>
  

Linha 127-130: JS condicional antes do </body>
  
    <script src="/js/chat-widget.js?v=..." defer></script>
  

Linha 194-206: HTML condicional + fallback GHL
  
    
      

<div id="cw-overlay" aria-hidden="true"></div>


<button id="cw-toggle" aria-label="Open chat" aria-haspopup="dialog" style="opacity:0;pointer-events:none">
  <span id="cw-badge" aria-hidden="true">1</span>
  <svg class="icon-chat" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
    <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
  </svg>
  <svg class="icon-close" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
    <line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
  </svg>
</button>


<div id="cw-window" role="dialog" aria-modal="true" aria-label="Chat with Suaid Global" style="opacity:0;pointer-events:none">
  
  <div class="cw-header">
    <button class="cw-restart" id="cw-restart-btn" onclick="restartFlow()" aria-label="Restart conversation" title="Restart">
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg>
    </button>
    <button class="cw-minimize" onclick="toggleChat()" aria-label="Minimize chat">
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><polyline points="6 9 12 15 18 9"/></svg>
    </button>
    <div class="cw-header-top">
      <div class="cw-avatar" aria-hidden="true">
        <svg viewBox="0 0 834 834" fill="none" xmlns="http://www.w3.org/2000/svg">
          <circle cx="417" cy="417" r="417" fill="#142A43"/>
          <text x="417" y="460" text-anchor="middle" fill="#E8611A" font-size="380" font-weight="800" font-family="Inter,sans-serif">S</text>
        </svg>
      </div>
      <div class="cw-header-info">
        <h3>Suaid Global</h3>
        <div class="cw-status">
          <span class="cw-status-dot" aria-hidden="true"></span>
          <span>Typically replies in minutes</span>
        </div>
      </div>
    </div>
  </div>

  
  <div class="cw-progress" role="progressbar" aria-label="Chat progress" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"><div class="cw-progress-bar" id="cw-progress" style="width:0%"></div></div>

  
  <div class="cw-messages" id="cw-messages" aria-live="polite" aria-relevant="additions"></div>

  
  <div class="cw-input-area" id="cw-input-area">
    <div class="cw-input-wrap" id="cw-input-wrap">
      <div id="cw-phone-region" style="display:none"></div>
      <input type="text" class="cw-input" id="cw-input" placeholder="Type your message..." autocomplete="off" aria-label="Chat message input">
      <button class="cw-send-btn" id="cw-send" aria-label="Send message">
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
          <line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/>
        </svg>
      </button>
    </div>
    <div class="cw-input-error" id="cw-error" role="alert" aria-live="assertive"></div>
  </div>

  
  <div class="cw-powered">Powered by <a href="https://suaidglobal.com" target="_blank" rel="noopener">Suaid Global</a></div>
</div>

    
  

Flag useCustomWidget

Atualmente definida apenas em src/support/faq/shipping.njk (frontmatter: useCustomWidget: true). Todas as outras páginas usam o GHL widget padrão.

Isso é intencional — permite testar o widget custom em uma única página antes de ativar globalmente.


2. PROBLEMAS ENCONTRADOS

🔴 CRÍTICO — Endpoint do Worker inconsistente no IMPLEMENTATION-PROMPT antigo

O IMPLEMENTATION-PROMPT.md antigo referenciava suaid-form-proxy.suaidglobal.workers.dev, mas o código real em chat-widget.js e chat-widget.html aponta para:

const API_ENDPOINT = 'https://suaid-form-proxy.suaid.workers.dev';

O correto é suaid.workers.dev (mesmo domínio usado pelo tariff-tool.js). Sem conflito — o código está correto.

🟡 ATENÇÃO — Widget ativo em apenas 1 página

O useCustomWidget: true está somente em src/support/faq/shipping.njk. Para ativar globalmente existem duas opções:

Opção A — Ativar via data global (recomendado para ir ao ar): Criar src/_data/useCustomWidget.js que retorna true:

module.exports = true;

Isso define useCustomWidget = true globalmente em todas as páginas, ativando o widget custom e desativando o GHL widget em todo o site.

Opção B — Ativar página por página (testing): Adicionar useCustomWidget: true no frontmatter de cada página que deve usar o widget custom. Útil para rollout gradual.

Opção C — Remover a flag e ativar direto no base.njk (all-in): Remover os `` e sempre carregar o widget custom, removendo completamente o GHL widget. Só fazer isso quando o widget estiver 100% validado.

🟡 ATENÇÃO — Sem suporte a Dark Mode no widget

O site suporta dark mode via [data-theme=dark]. O widget CSS usa variáveis --cw-* com valores fixos (tema claro). Se o usuário estiver em dark mode, o widget aparecerá com fundo branco contrastando com o site escuro.

Impacto: Visual inconsistente em dark mode. Solução: Adicionar bloco [data-theme=dark] no chat-widget.css com variáveis adaptadas.

🟡 ATENÇÃO — ALLOWED_ORIGINS no Worker

O worker aceita origins de:

Se o site for acessado via outro domínio (ex: preview do Cloudflare Pages como xxx.pages.dev), o widget falhará silenciosamente (CORS blocked). Adicionar o domínio de preview se necessário.

🟢 OK — Isolamento CSS

Todos os seletores CSS usam prefixo #cw- ou .cw-. Variáveis usam --cw-*. Zero conflito com style.css.

🟢 OK — Isolamento JS

O JS não expõe nenhuma variável global exceto toggleChat e restartFlow (necessárias para onclick no HTML). Não conflita com main.js.

🟢 OK — Progressive Sync (4 pontos)

O syncToGHL está implementado em 4 etapas:

  1. Linha 450: após email → cria Contact + Opportunity
  2. Linha 473: após phone → atualiza com telefone
  3. Linha 501: após details → atualiza com detalhes
  4. Linha 572: final (contact method) → syncToGHL(true) marca como completo

🟢 OK — Worker com update logic

O worker suporta _contactId e _opportunityId no payload para fazer PUT ao invés de POST em syncs subsequentes.


3. MELHORIAS RECOMENDADAS

3.1 🔴 PRIORIDADE ALTA — Dark Mode

Adicionar ao final de chat-widget.css:

/* Dark mode support */
[data-theme=dark] #cw-toggle{
  box-shadow:0 4px 20px rgba(232,97,26,0.3);
}
[data-theme=dark] #cw-window{
  --cw-bg:#1A2332;
  --cw-bg-alt:#0F1922;
  --cw-border:rgba(255,255,255,0.08);
  --cw-text:#F1F5F9;
  --cw-text-sec:#94A3B8;
  --cw-text-muted:#64748B;
  --cw-shadow:0 8px 40px rgba(0,0,0,0.4);
  border:1px solid rgba(255,255,255,0.06);
}
[data-theme=dark] .cw-msg.bot .cw-bubble{
  background:var(--cw-bg-alt);
  color:var(--cw-text);
}
[data-theme=dark] .cw-opt-btn{
  background:var(--cw-bg-alt);
  color:var(--cw-text);
  border-color:rgba(255,255,255,0.1);
}
[data-theme=dark] .cw-opt-btn:hover{
  border-color:var(--cw-orange);
}
[data-theme=dark] .cw-input-wrap{
  background:var(--cw-bg-alt);
  border-color:rgba(255,255,255,0.1);
}
[data-theme=dark] .cw-input{
  color:var(--cw-text);
}
[data-theme=dark] .cw-contact-card{
  background:var(--cw-bg-alt);
  border-color:rgba(255,255,255,0.08);
}
[data-theme=dark] .cw-summary{
  background:var(--cw-bg-alt);
  border-color:rgba(255,255,255,0.08);
}
[data-theme=dark] .cw-powered{
  border-color:rgba(255,255,255,0.04);
  color:var(--cw-text-muted);
}
[data-theme=dark] .cw-region-dropdown{
  background:var(--cw-bg);
  border-color:rgba(255,255,255,0.1);
}
[data-theme=dark] .cw-region-item:hover{
  background:rgba(255,255,255,0.05);
}

3.2 🔴 PRIORIDADE ALTA — Retry Queue para leads perdidos

Atualmente, se syncToGHL() falha, salva em sessionStorage mas nunca tenta reenviar. Adicionar retry automático:

// No chat-widget.js, após a definição de syncToGHL:
// Retry pending leads on page load
(function retryPendingLeads(){
  try {
    const pending = sessionStorage.getItem('cw_pending_lead');
    if(!pending) return;
    const payload = JSON.parse(pending);
    fetch(API_ENDPOINT, {
      method: 'POST',
      headers: {'Content-Type':'application/json'},
      body: JSON.stringify(payload),
    }).then(r => {
      if(r.ok) sessionStorage.removeItem('cw_pending_lead');
    }).catch(() => {}); // silent fail, will retry next page
  } catch(e){}
})();

3.3 🟡 PRIORIDADE MÉDIA — RTL (Arabic) support

O widget usa position: fixed o que funciona em RTL, mas os botões e layout interno devem ser espelhados. Adicionar:

[dir="rtl"] #cw-toggle{right:auto;left:24px}
[dir="rtl"] #cw-window{right:auto;left:24px;transform-origin:bottom left}
[dir="rtl"] .cw-msg.user{flex-direction:row}
[dir="rtl"] .cw-msg.bot{flex-direction:row-reverse}
[dir="rtl"] .cw-header .cw-minimize{right:auto;left:16px}
[dir="rtl"] .cw-header::after{right:auto;left:-40px}

3.4 🟡 PRIORIDADE MÉDIA — Adaptar ao padrão visual do site

O widget tem seu próprio design system. Para ficar 100% alinhado ao site, considerar:

Radius: Widget usa --cw-radius: 20px, site usa --radius: 16px. Alinhar para 16px.

Sombras: Widget usa 0 8px 40px rgba(0,0,0,0.15), site usa --shadow-md: 0 4px 20px rgba(0,0,0,.06). O widget é mais dramático (intencional para floating element).

Cards: O site usa border-radius: 16px e hover: translateY(-3px) em cards. O widget usa 12px nos option buttons. Alinhar os .cw-opt-btn para border-radius: var(--cw-radius) (~16px) e adicionar hover lift.

Font size: O site base é 16px. O widget messages usam .82rem (~13px). Pode ser levemente pequeno em telas grandes — considerar .85rem.

3.5 🟡 PRIORIDADE MÉDIA — Acessibilidade (a11y)

3.6 🟢 PRIORIDADE BAIXA — Animação de entrada contextual

Carregar o widget com delay de 3-5s após page load, e mostrar o badge pulsando. Atualmente aparece imediatamente com o DOM.

// Delay widget appearance
document.addEventListener('DOMContentLoaded', () => {
  setTimeout(() => {
    document.getElementById('cw-toggle').classList.add('ready');
  }, 3000);
});
#cw-toggle{opacity:0;transform:scale(0.5);pointer-events:none}
#cw-toggle.ready{opacity:1;transform:scale(1);pointer-events:auto;transition:all .5s cubic-bezier(.4,0,.2,1)}

3.7 🟢 PRIORIDADE BAIXA — GA4 Events avançados

Atualmente só rastreia generate_lead no final. Adicionar eventos em cada step para funnel analysis:

function trackStep(step) {
  if(typeof gtag === 'function'){
    gtag('event', 'chat_widget_step', {
      event_category: 'chat_widget',
      step_name: step,
      objective: state.data.objective || 'not_set'
    });
  }
}

Chamar trackStep(state.step) no updateProgress().


4. COMO ATIVAR GLOBALMENTE (quando pronto)

Passo 1 — Testar na página de shipping FAQ

A flag já está ativa. Testar o flow completo, verificar console, confirmar no GHL que o contato + oportunidade foram criados.

Passo 2 — Ativar globalmente

Criar src/_data/useCustomWidget.js:

module.exports = true;

Isso ativa o widget em TODAS as páginas (exceto /quote/ que já é excluída no base.njk).

Passo 3 — Remover GHL widget fallback (opcional, após validação)

No base.njk, remover o bloco else com o script do LeadConnector:


Passo 4 — Deploy do Worker atualizado

O worker em src/chat-widget/suaid-form-proxy-worker.js contém a lógica de progressive sync (_contactId e _opportunityId). Precisa ser deployed no Cloudflare:

# Via Wrangler CLI
npx wrangler deploy src/chat-widget/suaid-form-proxy-worker.js \
  --name suaid-form-proxy \
  --compatibility-date 2024-01-01

# Ou copiar manualmente no Cloudflare Dashboard:
# Workers & Pages → suaid-form-proxy → Edit Code → paste → Deploy

IMPORTANTE: O worker atualizado é backward-compatible — funciona tanto com o quote form existente quanto com o chat widget. O quote form NÃO envia _contactId/_opportunityId, então o worker segue o fluxo original (search + create).


5. FLUXO DE DADOS — ZERO LEAD LOSS

LEAD ABRE CHAT
  ↓
[1] Escolhe objetivo (welcome → objective)
  ↓
[2] Digita nome (objective → name)
  ↓
[3] Digita email → VALIDA → syncToGHL() ━━━► Worker ━━━► GHL
  ↓                         ↑ cria Contact     ↑ retorna contactId
  ↓                         ↑ cria Opportunity  ↑ retorna opportunityId
  ↓                         ↑ salva IDs no state
  ↓
[4] Digita telefone → VALIDA → syncToGHL() ━━━► Worker ━━━► GHL
  ↓                             ↑ atualiza Contact (PUT com contactId)
  ↓                             ↑ atualiza Opportunity (PUT com oppId)
  ↓
[5] Descreve necessidade → syncToGHL() ━━━► Worker ━━━► GHL
  ↓                         ↑ atualiza com detalhes
  ↓
[6] Escolhe método de contato → syncToGHL(true) ━━━► Worker ━━━► GHL
  ↓                               ↑ sync final (isFinal=true)
  ↓
[7] Mostra summary + thank you + links diretos

Se o lead abandona em qualquer ponto após o step 3, os dados já estão no GHL. Cada sync subsequente ATUALIZA o mesmo contato/oportunidade (não cria duplicatas).

Se a rede falha, os dados são salvos em sessionStorage para retry (melhoria 3.2 para retry automático recomendada).


6. DADOS ENVIADOS AO GHL

Contact (criado/atualizado)

Opportunity (criada/atualizada)

Tracking enriquecido (no campo notes da Opportunity)

Server-side enrichment (Cloudflare Worker)

Se o client-side não conseguir IP/geo (bloqueio de ipapi.co), o worker enriquece com:


7. ESTRUTURA DE ARQUIVOS

src/
├── css/
│   ├── style.css              ← NÃO MODIFICADO
│   ├── phosphor.css           ← NÃO MODIFICADO
│   └── chat-widget.css        ← NOVO (464 linhas)
│
├── js/
│   ├── main.js                ← NÃO MODIFICADO
│   └── chat-widget.js         ← NOVO (748 linhas)
│
├── _includes/
│   ├── layouts/
│   │   └── base.njk           ← MODIFICADO (3 blocos condicionais adicionados)
│   └── partials/
│       └── chat-widget.njk    ← NOVO (59 linhas, HTML estrutural)
│
├── chat-widget/
│   ├── chat-widget.html       ← Demo standalone (~1350 linhas, para testes offline)
│   ├── suaid-form-proxy-worker.js  ← Worker atualizado (progressive sync)
│   └── IMPLEMENTATION-PROMPT.md    ← ESTE ARQUIVO
│
├── _data/
│   └── useCustomWidget.js     ← CRIAR quando pronto para ativar globalmente

8. CHECKLIST ANTES DE ATIVAR GLOBALMENTE

Funcional

Visual

Sem quebrar o site

Performance


9. RESUMO DE PRIORIDADES

#MelhoriaPrioridadeImpactoEsforço
1Dark mode support🔴 AltaVisual quebrado em dark mode~30 linhas CSS
2Retry queue para leads🔴 AltaLeads perdidos se rede falha~15 linhas JS
3RTL (Arabic) support🟡 MédiaWidget invertido em ar~10 linhas CSS
4Alinhar radius/shadows ao site🟡 MédiaConsistência visual~5 variáveis CSS
5Acessibilidade (a11y)🟡 MédiaCompliance, screen readers~20 linhas HTML+JS
6Delay de entrada (3s)🟢 BaixaUX mais elegante~10 linhas CSS+JS
7GA4 funnel events🟢 BaixaAnalytics detalhado~10 linhas JS
8Ativar globalmente🟡 MédiaWidget em todo o site1 arquivo JS de 1 linha
9Deploy worker atualizado🔴 AltaProgressive sync funcionar1 comando wrangler