Criar uma interface de chat personalizada usando a API do cliente do agente

Este artigo explica como criar uma interface de chat totalmente personalizada para seu site de Power Pages usando a API do cliente agent. Ao contrário do widget de chat padrão, essa abordagem oferece controle total sobre o design da interface do usuário, o estilo e a experiência do usuário.

Etapa 1: Criar um agente de site

  1. Entre no Power Pages.

  2. Selecione + Editar.

  3. Vá para a área de trabalho Configurar e selecione Agentes.

  4. Habilite o Agente do site usando o botão de alternância para criar o agente.

    Screenshot que mostra como habilitar o agente de site em Power Pages.

  5. Após o provisionamento bem-sucedido, Power Pages adiciona um novo agente com o nome do site acrescentado com "bot". O agente aparece na seção Agents da lista de sites. Selecione os três pontos verticais ao lado do agente recém-provisionado e selecione Editar.

    Captura de tela que mostra o menu do agente e a opção Editar.

  6. Mantenha a alternância Mostrar no Widget de Chat definida como Ativada.

    Note

    Quando definido como Desativado, o agente não está disponível para nenhum usuário.

  7. Selecione a função Administrador para usuários que podem interagir com o agente.

  8. Copie o nome do esquema a ser usado posteriormente.

  9. Clique em Salvar.

Etapa 2. Criar página da Web

  1. Inicie o estúdio de design do Power Pages. Para obter mais informações, consulte Usar o estúdio de design.

  2. No workspace Páginas, selecione + Página.

  3. Em Descrever uma página para criá-la , selecione Outras maneiras de adicionar uma página.

  4. Na caixa de diálogo Adicionar uma página , insira detalhes para a página da seguinte maneira:

    • No campo Nome, insira Assistente Virtual e selecione o layout Iniciar do zero.
    • Selecione Adicionar.
  5. Selecione a opção Editar Código no canto superior direito.

  6. Na caixa de diálogo Editar no Visual Studio Code para Web, selecione Abrir Visual Studio Code.

  7. Substitua o código encontrado na página HTML pelo seguinte:

    <div class="row sectionBlockLayout text-start" style="min-height:auto;padding:8px">
    <div class="container" style="display:flex;flex-wrap:wrap">
      <div class="col-lg-12 columnBlockLayout" style="padding:0;margin:0;min-height:200px">

        <style>
          #va-shell {
            --accent: #0f6cbd;
            --accent-dk: #0c57a8;
            --bg: #f5f7fa;
            --bot-bg: #f0f4f9;
            --fg: #1b2333;
            --border: #e2e8f0;

            display: flex;
            flex-direction: column;
            max-width: 860px;
            height: calc(100vh - 150px);
            min-height: 480px;
            margin: 20px auto;
            background: #fff;
            border-radius: 16px;
            box-shadow: 0 4px 24px rgba(0, 0, 0, .10);
            border: 1px solid var(--border);
            overflow: hidden;
            font: 15px/1.55 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
            color: var(--fg);
          }

          /* Transcript */
          #va-transcript {
            flex: 1;
            min-height: 0;
            overflow-y: auto;
            padding: 20px;
            background: var(--bg);
            display: flex;
            flex-direction: column;
            gap: 4px;
            scroll-behavior: smooth;
          }

          #va-transcript::-webkit-scrollbar {
            width: 6px;
          }

          #va-transcript::-webkit-scrollbar-thumb {
            background: #c8d0db;
            border-radius: 3px;
          }

          /* Message rows */
          .msg {
            display: flex;
            align-items: flex-end;
            gap: 8px;
            max-width: 78%;
            animation: fadeUp .18s ease-out;
          }

          .msg.user {
            flex-direction: row-reverse;
            align-self: flex-end;
          }

          .msg.bot,
          .msg.error {
            align-self: flex-start;
          }

          @keyframes fadeUp {
            from {
              opacity: 0;
              transform: translateY(6px);
            }

            to {
              opacity: 1;
              transform: none;
            }
          }

          .bot-icon {
            width: 28px;
            height: 28px;
            border-radius: 50%;
            background: var(--accent);
            color: #fff;
            display: grid;
            place-items: center;
            flex-shrink: 0;
          }

          .bot-icon svg {
            width: 14px;
            height: 14px;
          }

          .bubble {
            padding: 10px 14px;
            border-radius: 10px;
            word-break: break-word;
          }

          .user .bubble {
            background: var(--accent);
            color: #fff;
            border-bottom-right-radius: 3px;
          }

          .bot .bubble {
            background: var(--bot-bg);
            border-bottom-left-radius: 3px;
          }

          .error .bubble {
            background: #fef2f2;
            color: #b91c1c;
            border: 1px solid #fca5a5;
            border-bottom-left-radius: 3px;
          }

          .bubble p {
            margin: 0 0 .5em;
          }

          .bubble p:last-child {
            margin: 0;
          }

          .bubble strong {
            font-weight: 600;
          }

          .bubble code {
            font-family: Consolas, monospace;
            font-size: .88em;
            background: rgba(0, 0, 0, .08);
            padding: 1px 5px;
            border-radius: 4px;
          }

          .user .bubble code {
            background: rgba(255, 255, 255, .2);
          }

          .bubble pre {
            background: #1e293b;
            color: #e2e8f0;
            padding: 12px;
            border-radius: 8px;
            overflow-x: auto;
            margin: 8px 0;
            font-size: .85em;
          }

          .bubble pre code {
            background: none;
            padding: 0;
          }

          .bubble a {
            color: inherit;
            text-decoration: underline;
            text-underline-offset: 2px;
          }

          .bubble ul,
          .bubble ol {
            margin: 6px 0;
            padding-left: 20px;
          }

          .ts {
            font-size: 11px;
            opacity: .5;
            margin-top: 4px;
            display: block;
          }

          .user .ts {
            text-align: right;
          }

          /* Typing indicator */
          #va-typing {
            display: none;
            align-items: flex-end;
            gap: 8px;
            align-self: flex-start;
            padding-bottom: 4px;
          }

          #va-typing.show {
            display: flex;
            animation: fadeUp .18s ease-out;
          }

          #va-dots {
            background: var(--bot-bg);
            border-radius: 10px;
            border-bottom-left-radius: 3px;
            padding: 12px 16px;
            display: flex;
            gap: 5px;
          }

          .dot {
            width: 7px;
            height: 7px;
            border-radius: 50%;
            background: #94a3b8;
            animation: bounce 1.2s infinite;
          }

          .dot:nth-child(2) {
            animation-delay: .2s;
          }

          .dot:nth-child(3) {
            animation-delay: .4s;
          }

          @keyframes bounce {

            0%,
            60%,
            100% {
              transform: translateY(0);
              opacity: .5;
            }

            30% {
              transform: translateY(-5px);
              opacity: 1;
            }
          }

          /* Date divider */
          .divider {
            display: flex;
            align-items: center;
            gap: 10px;
            color: #94a3b8;
            font-size: 12px;
            margin: 10px 0 6px;
            user-select: none;
          }

          .divider::before,
          .divider::after {
            content: '';
            flex: 1;
            height: 1px;
            background: var(--border);
          }

          /* Composer */
          #va-composer {
            padding: 12px 16px 10px;
            background: #fff;
            border-top: 1px solid var(--border);
            flex-shrink: 0;
          }

          #va-wrap {
            display: flex;
            align-items: flex-end;
            gap: 8px;
            background: var(--bg);
            border: 1.5px solid var(--border);
            border-radius: 12px;
            padding: 8px 8px 8px 14px;
            transition: border-color .15s, box-shadow .15s;
          }

          #va-wrap:focus-within {
            border-color: var(--accent);
            box-shadow: 0 0 0 3px rgba(15, 108, 189, .12);
          }

          #va-input {
            flex: 1;
            border: none;
            background: none;
            outline: none;
            resize: none;
            font: inherit;
            color: inherit;
            max-height: 160px;
            overflow-y: auto;
          }

          #va-input::placeholder {
            color: #94a3b8;
          }

          #va-input:disabled {
            opacity: .55;
          }

          #va-send {
            width: 38px;
            height: 38px;
            border-radius: 9px;
            border: none;
            background: var(--accent);
            color: #fff;
            cursor: pointer;
            display: grid;
            place-items: center;
            flex-shrink: 0;
            transition: background .15s, transform .1s, opacity .15s;
          }

          #va-send:hover:not(:disabled) {
            background: var(--accent-dk);
            transform: scale(1.05);
          }

          #va-send:disabled {
            opacity: .4;
            cursor: not-allowed;
          }

          #va-hint {
            margin: 6px 0 0;
            font-size: 12px;
            color: #94a3b8;
            text-align: center;
          }

          #va-hint kbd {
            background: #f1f5f9;
            border: 1px solid #cbd5e1;
            border-radius: 4px;
            padding: 1px 5px;
            font-size: 11px;
            font-family: inherit;
            color: #475569;
          }

          @media (max-width: 640px) {
            #va-shell {
              height: 100svh;
              min-height: unset;
              margin: 0;
              border-radius: 0;
              border-inline: none;
            }

            .msg {
              max-width: 92%;
            }

            #va-hint {
              display: none;
            }
          }
        </style>

        <div id="va-shell">

          <main id="va-transcript" role="log" aria-live="polite" aria-label="Chat transcript">
            <div id="va-typing" role="status" aria-label="Assistant is typing">
              <div class="bot-icon" aria-hidden="true">
                <svg viewBox="0 0 24 24" fill="currentColor">
                  <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 15v-4H7l5-8v4h4l-5 8z" />
                </svg>
              </div>
              <div id="va-dots" aria-hidden="true">
                <span class="dot"></span><span class="dot"></span><span class="dot"></span>
              </div>
            </div>
          </main>

          <footer id="va-composer">
            <div id="va-wrap">
              <textarea id="va-input" placeholder="Ask me anything…" rows="1" maxlength="4000" spellcheck="true"
                aria-label="Message input"></textarea>
              <button id="va-send" type="button" aria-label="Send message" disabled>
                <svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
                  <path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z" />
                </svg>
              </button>
            </div>
            <p id="va-hint">Press <kbd>Enter</kbd> to send &nbsp;·&nbsp; <kbd>Shift</kbd>+<kbd>Enter</kbd> for new line
              &nbsp;·&nbsp; <kbd>Ctrl</kbd>+<kbd>N</kbd> for new conversation</p>
          </footer>

        </div>

        <script>
          (function () {
            'use strict';

            const AGENT_SCHEMA = '<Replace agent schema name>';
            const WELCOME = "Hello! I'm your Virtual Assistant. How can I help you today?";

            let msgs = [], busy = false;

            const el = id => document.getElementById(id);
            const transcript = el('va-transcript');
            const input = el('va-input');
            const sendBtn = el('va-send');
            const typing = el('va-typing');

            /* ── Safe markdown renderer ── */
            const md = (() => {
              const esc = s => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
              const href = u => { try { return /^https?:|mailto:/.test(new URL(u).protocol) ? esc(u) : '#'; } catch { return '#'; } };
              const span = s => s
                .replace(/`([^`]+)`/g, (_, c) => `<code>${c}</code>`)
                .replace(/\*\*(.+?)\*\*/g, (_, t) => `<strong>${t}</strong>`)
                .replace(/\*(.+?)\*/g, (_, t) => `<em>${t}</em>`)
                .replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, l, u) => `<a href="${href(u)}" target="_blank" rel="noopener">${l}</a>`);

              return {
                esc, render: raw => {
                  if (!raw) return '';
                  const blocks = [];
                  const src = raw.replace(/\r\n/g, '\n')
                    .replace(/```(\w*)\n?([\s\S]*?)```/g, (_, l, c) => (blocks.push({ l, c }), `\x00${blocks.length - 1}\x00`));

                  let html = '', list = [], lt = '';
                  const flush = () => {
                    if (!list.length) return;
                    html += `<${lt}>${list.map(i => `<li>${span(i)}</li>`).join('')}</${lt}>`;
                    list = []; lt = '';
                  };

                  src.split('\n').forEach(line => {
                    if (/^\x00\d+\x00$/.test(line)) {
                      flush();
                      const { l, c } = blocks[+line.replace(/\x00/g, '')];
                      html += `<pre><code${l ? ` class="language-${esc(l)}"` : ''}>${esc(c)}</code></pre>`;
                      return;
                    }
                    const ul = line.match(/^[*-] (.+)/);
                    const ol = line.match(/^\d+\. (.+)/);
                    if (ul) { if (lt && lt !== 'ul') flush(); lt = 'ul'; list.push(ul[1]); return; }
                    if (ol) { if (lt && lt !== 'ol') flush(); lt = 'ol'; list.push(ol[1]); return; }
                    flush();
                    const h = line.match(/^(#{1,3}) (.+)/);
                    if (h) { const n = h[1].length + 2; html += `<h${n}>${span(h[2])}</h${n}>`; return; }
                    if (/^(-{3,}|\*{3,})$/.test(line.trim())) { html += '<hr>'; return; }
                    html += line.trim() ? `<p>${span(line)}</p>` : '<br>';
                  });
                  flush();
                  return html.replace(/(<br>){2,}/g, '<br>');
                }
              };
            })();

            /* ── Render a message bubble ── */
            function bubble(msg) {
              const row = document.createElement('div');
              row.className = `msg ${msg.role}`;

              const plain = msg.role === 'user' || msg.role === 'error' || msg.format === 'plain';
              const content = plain
                ? `<p>${md.esc(msg.text).replace(/\n/g, '<br>')}</p>`
                : md.render(msg.text);
              const icon = `<div class="bot-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 15v-4H7l5-8v4h4l-5 8z"/></svg></div>`;
              const d = new Date(msg.time);
              const h = d.getHours() % 12 || 12;
              const ts = `${h}:${String(d.getMinutes()).padStart(2, '0')} ${d.getHours() < 12 ? 'AM' : 'PM'}`;

              row.innerHTML = `${msg.role !== 'user' ? icon : ''}<div class="bubble">${content}<time class="ts">${ts}</time></div>`;
              transcript.insertBefore(row, typing);
              transcript.scrollTop = transcript.scrollHeight;
            }

            function addDivider() {
              const d = document.createElement('div');
              d.className = 'divider';
              d.textContent = new Date().toLocaleDateString([], { weekday: 'long', month: 'long', day: 'numeric' });
              transcript.insertBefore(d, typing);
            }

            /* ── State helpers ── */
            const setBusy = b => { busy = b; input.disabled = b; sendBtn.disabled = b || !input.value.trim(); };
            const showTyping = () => { typing.classList.add('show'); transcript.scrollTop = transcript.scrollHeight; };
            const hideTyping = () => { typing.classList.remove('show'); };

            /* ── Send ── */
            function send() {
              const text = input.value.trim();
              if (!text || busy) return;

              msgs.push({ role: 'user', text, format: 'plain', time: new Date() });
              bubble(msgs[msgs.length - 1]);
              input.value = ''; resize(); setBusy(true); showTyping();

              if (typeof window.$pages?.agent?.SendActivity !== 'function') {
                console.error('[VA] $pages.agent.SendActivity not available. Add the Agent component to this page in Power Pages Studio.');
                return onError({ message: '$pages.agent not available' });
              }
              try {
                window.$pages.agent.SendActivity(AGENT_SCHEMA, { text }, onResponse, onError);
              } catch (e) { console.error('[VA]', e); onError(e); }
            }

            function onResponse(r) {
              hideTyping(); setBusy(false);
              if (!r || r.type !== 'message' || !r.text) return;
              const m = { role: 'bot', text: r.text, format: (r.textFormat || 'markdown').toLowerCase(), time: new Date() };
              msgs.push(m); bubble(m); input.focus();
            }

            function onError(e) {
              console.error('[VA] Agent error:', e);
              hideTyping(); setBusy(false);
              const text = e?.message?.includes('$pages.agent')
                ? 'The Virtual Assistant integration isn\'t available here. Ensure the Agent component is configured in Power Pages Studio for this page.'
                : 'Sorry \u2014 I couldn\'t reach the assistant. Please try again.';
              const m = { role: 'error', text, format: 'plain', time: new Date() };
              msgs.push(m); bubble(m); input.focus();
            }

            /* ── Clear chat ── */
            function clearChat() {
              msgs = [];
              Array.from(transcript.children).forEach(c => { if (c !== typing) transcript.removeChild(c); });
              addDivider();
              const w = { role: 'bot', text: WELCOME, format: 'plain', time: new Date() };
              msgs.push(w); bubble(w); input.focus();
            }

            /* ── Auto-resize textarea ── */
            function resize() {
              input.style.height = 'auto';
              input.style.height = Math.min(input.scrollHeight, 160) + 'px';
            }

            /* ── Init ── */
            sendBtn.addEventListener('click', send);
            input.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(); } });
            input.addEventListener('input', () => { resize(); sendBtn.disabled = busy || !input.value.trim(); });
            document.addEventListener('keydown', e => { if ((e.ctrlKey || e.metaKey) && e.key === 'n') { e.preventDefault(); clearChat(); } });

            addDivider();
            const w = { role: 'bot', text: WELCOME, format: 'plain', time: new Date() };
            msgs.push(w); bubble(w); input.focus();

          })();
        </script>

      </div>
    </div>
  </div>  
  1. Atualize a variável AGENT_SCHEMA com o valor copiado anteriormente.
  2. Salve o arquivo.
  3. Retorne ao designer do Power Pages.
  4. Em Você está editando código em Visual Studio Code para a caixa de diálogo Web, siga as instruções e selecione o botão Sync.

Etapa 3: Ocultar o widget de chat padrão

Substitua o estilo, conforme mostrado no código a seguir, no modelo do cabeçalho para ocultar o widget padrão, de forma que a página tenha duas interfaces de chat.

  /* Hide the default floating bot widget */
  .pva-floating-style { display: none !important; }

Etapa 4: Iniciar conversa com o agente

Para testar a funcionalidade do agente:

  1. Selecione Visualizar e escolha Área de Trabalho.
  2. Entre em seu site com a conta de usuário atribuída. Como selecionamos a função Administradores na etapa anterior, neste exemplo, entre como administrador.
  3. Vá para a página da Web Assistente Virtual criada anteriormente.
  4. Verifique as operações.
  • Personalizar seu agente