Skip to content

Widget de WebChat - Guia de Integração

Este guia mostra como integrar o iFriend Agent no seu website através de um widget de chat.

🎯 Visão Geral

O WebChat Adapter permite que você integre o agente diretamente em seu website através de requisições HTTP/REST síncronas, sem necessidade de webhooks ou configurações complexas.

Características

  • HTTP/REST síncrono: Resposta na mesma requisição
  • Sem webhooks: Não precisa expor endpoints públicos
  • CORS configurável: Suporte nativo a requisições cross-origin
  • Sessões persistentes: Mantém contexto da conversa
  • Plug-and-play: Configuração mínima necessária

🚀 Quick Start

1. Configurar Backend

No seu arquivo .env:

# Ativar WebChat
WEBCHAT_ENABLED=true

# Opcional: customizar endpoint
WEBCHAT_WEBHOOK_PATH=/webchat/message

# Opcional: CORS (padrão: permite tudo)
WEBCHAT_ALLOWED_ORIGINS=https://seusite.com,https://www.seusite.com
WEBCHAT_ENABLE_CORS=true

Ou adicionar diretamente em MESSAGING_PLATFORMS:

MESSAGING_PLATFORMS=slack,webchat

2. Iniciar Servidor

python unified_bot.py

O endpoint estará disponível em: http://localhost:8080/webchat/message

3. Testar com cURL

curl -X POST http://localhost:8080/webchat/message \
  -H "Content-Type: application/json" \
  -d '{
    "user_id": "user123",
    "message": "Olá! Quero fazer uma viagem para Paris"
  }'

Resposta esperada:

{
  "response": "Olá! Que ótimo que você quer viajar para Paris! ...",
  "session_id": "webchat_user123_a1b2c3d4",
  "metadata": {
    "platform": "webchat",
    "user_id": "user123"
  }
}

📝 API Reference

Endpoint

POST /webchat/message

Request

interface WebChatRequest {
  user_id: string;           // Obrigatório: ID único do usuário
  message: string;           // Obrigatório: Mensagem do usuário
  session_id?: string;       // Opcional: ID da sessão (auto-gerado se ausente)
  metadata?: {               // Opcional: Dados extras
    page_url?: string;
    user_name?: string;
    user_email?: string;
    [key: string]: any;
  }
}

Response

interface WebChatResponse {
  response: string;          // Resposta do agente
  session_id: string;        // ID da sessão (guardar para próximas msgs)
  metadata: {
    platform: "webchat";
    user_id: string;
  }
}

Códigos de Status

  • 200: Sucesso
  • 400: Requisição inválida (faltando user_id ou message)
  • 500: Erro interno do servidor
  • 503: WebChat não configurado

💻 Exemplo de Widget HTML/JavaScript

Exemplo Completo

Salve como webchat-widget.html:

<!DOCTYPE html>
<html lang="pt-BR">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>iFriend WebChat Widget</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
            padding: 20px;
            background: #f5f5f5;
        }

        .page-content {
            max-width: 800px;
            margin: 0 auto;
            background: white;
            padding: 40px;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
        }

        /* Chat Widget */
        #chat-widget {
            position: fixed;
            bottom: 20px;
            right: 20px;
            width: 400px;
            height: 600px;
            background: white;
            border-radius: 12px;
            box-shadow: 0 4px 20px rgba(0,0,0,0.15);
            display: flex;
            flex-direction: column;
            overflow: hidden;
            z-index: 1000;
        }

        #chat-widget.minimized {
            height: 60px;
        }

        /* Header */
        #chat-header {
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            padding: 15px 20px;
            display: flex;
            align-items: center;
            justify-content: space-between;
            cursor: pointer;
        }

        #chat-header h3 {
            margin: 0;
            font-size: 16px;
            font-weight: 600;
        }

        #chat-header .status {
            font-size: 12px;
            opacity: 0.9;
        }

        #minimize-btn {
            background: none;
            border: none;
            color: white;
            font-size: 20px;
            cursor: pointer;
            padding: 0;
            width: 24px;
            height: 24px;
            display: flex;
            align-items: center;
            justify-content: center;
        }

        /* Messages */
        #chat-messages {
            flex: 1;
            overflow-y: auto;
            padding: 20px;
            background: #f8f9fa;
        }

        .message {
            margin-bottom: 15px;
            display: flex;
            flex-direction: column;
        }

        .message.user {
            align-items: flex-end;
        }

        .message.agent {
            align-items: flex-start;
        }

        .message-bubble {
            max-width: 80%;
            padding: 10px 15px;
            border-radius: 18px;
            word-wrap: break-word;
        }

        .message.user .message-bubble {
            background: #667eea;
            color: white;
        }

        .message.agent .message-bubble {
            background: white;
            color: #333;
            border: 1px solid #e0e0e0;
        }

        .message-time {
            font-size: 11px;
            color: #999;
            margin-top: 5px;
            padding: 0 5px;
        }

        .typing-indicator {
            display: none;
            align-items: center;
            padding: 10px 15px;
        }

        .typing-indicator.active {
            display: flex;
        }

        .typing-dots {
            display: flex;
            gap: 4px;
        }

        .typing-dots span {
            width: 8px;
            height: 8px;
            border-radius: 50%;
            background: #999;
            animation: typing 1.4s infinite;
        }

        .typing-dots span:nth-child(2) {
            animation-delay: 0.2s;
        }

        .typing-dots span:nth-child(3) {
            animation-delay: 0.4s;
        }

        @keyframes typing {
            0%, 60%, 100% {
                transform: translateY(0);
            }
            30% {
                transform: translateY(-10px);
            }
        }

        /* Input */
        #chat-input-container {
            padding: 15px;
            background: white;
            border-top: 1px solid #e0e0e0;
        }

        #chat-input-form {
            display: flex;
            gap: 10px;
        }

        #chat-input {
            flex: 1;
            padding: 10px 15px;
            border: 1px solid #e0e0e0;
            border-radius: 24px;
            outline: none;
            font-size: 14px;
        }

        #chat-input:focus {
            border-color: #667eea;
        }

        #send-btn {
            background: #667eea;
            color: white;
            border: none;
            border-radius: 50%;
            width: 40px;
            height: 40px;
            cursor: pointer;
            display: flex;
            align-items: center;
            justify-content: center;
            transition: background 0.2s;
        }

        #send-btn:hover {
            background: #5568d3;
        }

        #send-btn:disabled {
            background: #ccc;
            cursor: not-allowed;
        }

        /* Mobile */
        @media (max-width: 480px) {
            #chat-widget {
                width: calc(100% - 20px);
                right: 10px;
                bottom: 10px;
                height: calc(100% - 20px);
            }

            #chat-widget.minimized {
                height: 60px;
            }
        }
    </style>
</head>
<body>
    <!-- Conteúdo da página -->
    <div class="page-content">
        <h1>Bem-vindo ao iFriend</h1>
        <p>Use o chat no canto inferior direito para conversar com nosso assistente de viagens!</p>
        <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit...</p>
    </div>

    <!-- Chat Widget -->
    <div id="chat-widget">
        <div id="chat-header" onclick="toggleChat()">
            <div>
                <h3>💬 iFriend Assistant</h3>
                <div class="status">Online</div>
            </div>
            <button id="minimize-btn" aria-label="Minimizar"></button>
        </div>

        <div id="chat-messages"></div>

        <div class="typing-indicator" id="typing-indicator">
            <div class="typing-dots">
                <span></span>
                <span></span>
                <span></span>
            </div>
        </div>

        <div id="chat-input-container">
            <form id="chat-input-form" onsubmit="sendMessage(event)">
                <input 
                    type="text" 
                    id="chat-input" 
                    placeholder="Digite sua mensagem..."
                    autocomplete="off"
                >
                <button type="submit" id="send-btn" aria-label="Enviar"></button>
            </form>
        </div>
    </div>

    <script>
        // Configuração
        const CONFIG = {
            apiUrl: 'http://localhost:8080/webchat/message',  // Altere para sua URL
            userId: generateUserId(),
            sessionId: null,
        };

        // Gera ID único para o usuário (persiste no localStorage)
        function generateUserId() {
            let userId = localStorage.getItem('ifriend_user_id');
            if (!userId) {
                userId = 'user_' + Math.random().toString(36).substr(2, 9);
                localStorage.setItem('ifriend_user_id', userId);
            }
            return userId;
        }

        // Recupera session_id do localStorage
        function getSessionId() {
            if (!CONFIG.sessionId) {
                CONFIG.sessionId = localStorage.getItem('ifriend_session_id');
            }
            return CONFIG.sessionId;
        }

        // Salva session_id no localStorage
        function saveSessionId(sessionId) {
            CONFIG.sessionId = sessionId;
            localStorage.setItem('ifriend_session_id', sessionId);
        }

        // Toggle chat minimize/maximize
        function toggleChat() {
            const widget = document.getElementById('chat-widget');
            widget.classList.toggle('minimized');
        }

        // Adiciona mensagem ao chat
        function addMessage(text, sender = 'agent') {
            const messagesContainer = document.getElementById('chat-messages');
            const messageDiv = document.createElement('div');
            messageDiv.className = `message ${sender}`;

            const now = new Date();
            const time = now.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' });

            messageDiv.innerHTML = `
                <div class="message-bubble">${escapeHtml(text)}</div>
                <div class="message-time">${time}</div>
            `;

            messagesContainer.appendChild(messageDiv);
            messagesContainer.scrollTop = messagesContainer.scrollHeight;
        }

        // Escape HTML para prevenir XSS
        function escapeHtml(text) {
            const div = document.createElement('div');
            div.textContent = text;
            return div.innerHTML.replace(/\n/g, '<br>');
        }

        // Mostra/esconde typing indicator
        function setTyping(isTyping) {
            const indicator = document.getElementById('typing-indicator');
            if (isTyping) {
                indicator.classList.add('active');
            } else {
                indicator.classList.remove('active');
            }
        }

        // Envia mensagem
        async function sendMessage(event) {
            event.preventDefault();

            const input = document.getElementById('chat-input');
            const sendBtn = document.getElementById('send-btn');
            const message = input.value.trim();

            if (!message) return;

            // Adiciona mensagem do usuário
            addMessage(message, 'user');
            input.value = '';

            // Desabilita input
            input.disabled = true;
            sendBtn.disabled = true;
            setTyping(true);

            try {
                const response = await fetch(CONFIG.apiUrl, {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                    },
                    body: JSON.stringify({
                        user_id: CONFIG.userId,
                        session_id: getSessionId(),
                        message: message,
                        metadata: {
                            page_url: window.location.href,
                            timestamp: new Date().toISOString(),
                        }
                    })
                });

                if (!response.ok) {
                    throw new Error(`HTTP error! status: ${response.status}`);
                }

                const data = await response.json();

                // Salva session_id
                if (data.session_id) {
                    saveSessionId(data.session_id);
                }

                // Adiciona resposta do agente
                setTyping(false);
                addMessage(data.response, 'agent');

            } catch (error) {
                console.error('Erro ao enviar mensagem:', error);
                setTyping(false);
                addMessage(
                    'Desculpe, ocorreu um erro ao processar sua mensagem. Tente novamente.',
                    'agent'
                );
            } finally {
                // Re-habilita input
                input.disabled = false;
                sendBtn.disabled = false;
                input.focus();
            }
        }

        // Mensagem de boas-vindas
        window.addEventListener('load', () => {
            setTimeout(() => {
                addMessage(
                    'Olá! Sou o iFriend, seu assistente de viagens. Como posso ajudar você hoje?',
                    'agent'
                );
            }, 500);
        });
    </script>
</body>
</html>

🔧 Exemplo com React

import React, { useState, useEffect, useRef } from 'react';

interface Message {
  text: string;
  sender: 'user' | 'agent';
  timestamp: Date;
}

const WebChatWidget: React.FC = () => {
  const [messages, setMessages] = useState<Message[]>([]);
  const [inputText, setInputText] = useState('');
  const [isLoading, setIsLoading] = useState(false);
  const [sessionId, setSessionId] = useState<string | null>(null);
  const messagesEndRef = useRef<HTMLDivElement>(null);

  const API_URL = 'http://localhost:8080/webchat/message';
  const USER_ID = getUserId();

  function getUserId() {
    let userId = localStorage.getItem('ifriend_user_id');
    if (!userId) {
      userId = 'user_' + Math.random().toString(36).substr(2, 9);
      localStorage.setItem('ifriend_user_id', userId);
    }
    return userId;
  }

  const scrollToBottom = () => {
    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
  };

  useEffect(() => {
    scrollToBottom();
  }, [messages]);

  useEffect(() => {
    // Mensagem de boas-vindas
    setMessages([{
      text: 'Olá! Sou o iFriend, seu assistente de viagens. Como posso ajudar você hoje?',
      sender: 'agent',
      timestamp: new Date()
    }]);
  }, []);

  const sendMessage = async (e: React.FormEvent) => {
    e.preventDefault();

    if (!inputText.trim() || isLoading) return;

    const userMessage: Message = {
      text: inputText,
      sender: 'user',
      timestamp: new Date()
    };

    setMessages(prev => [...prev, userMessage]);
    setInputText('');
    setIsLoading(true);

    try {
      const response = await fetch(API_URL, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          user_id: USER_ID,
          session_id: sessionId,
          message: inputText,
          metadata: {
            page_url: window.location.href,
          }
        })
      });

      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }

      const data = await response.json();

      if (data.session_id && !sessionId) {
        setSessionId(data.session_id);
      }

      const agentMessage: Message = {
        text: data.response,
        sender: 'agent',
        timestamp: new Date()
      };

      setMessages(prev => [...prev, agentMessage]);

    } catch (error) {
      console.error('Erro:', error);
      const errorMessage: Message = {
        text: 'Desculpe, ocorreu um erro. Tente novamente.',
        sender: 'agent',
        timestamp: new Date()
      };
      setMessages(prev => [...prev, errorMessage]);
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <div className="webchat-widget">
      <div className="messages">
        {messages.map((msg, idx) => (
          <div key={idx} className={`message ${msg.sender}`}>
            <div className="bubble">{msg.text}</div>
            <div className="time">
              {msg.timestamp.toLocaleTimeString('pt-BR', { 
                hour: '2-digit', 
                minute: '2-digit' 
              })}
            </div>
          </div>
        ))}
        {isLoading && <div className="typing-indicator">...</div>}
        <div ref={messagesEndRef} />
      </div>

      <form onSubmit={sendMessage}>
        <input
          type="text"
          value={inputText}
          onChange={(e) => setInputText(e.target.value)}
          placeholder="Digite sua mensagem..."
          disabled={isLoading}
        />
        <button type="submit" disabled={isLoading || !inputText.trim()}>
          Enviar
        </button>
      </form>
    </div>
  );
};

export default WebChatWidget;

🎨 Customização

Variáveis de Ambiente

# Backend URL (produção)
WEBCHAT_API_URL=https://api.ifriend.com/webchat/message

# CORS (permitir múltiplos domínios)
WEBCHAT_ALLOWED_ORIGINS=https://seusite.com,https://www.seusite.com

# Custom path
WEBCHAT_WEBHOOK_PATH=/api/chat

Estilos CSS

O widget HTML usa CSS customizável. Principais classes:

  • .chat-widget: Container principal
  • .message.user: Mensagens do usuário
  • .message.agent: Mensagens do agente
  • .typing-indicator: Indicador de digitação

🔒 Segurança

Autenticação (Opcional)

Para adicionar autenticação, modifique o metadata:

body: JSON.stringify({
  user_id: CONFIG.userId,
  message: message,
  metadata: {
    auth_token: getUserToken(),  // Token JWT do usuário
    page_url: window.location.href,
  }
})

E no backend (webchat_adapter.py), valide o token no metadata.

Rate Limiting

Considere adicionar rate limiting no backend para prevenir abuso.

📊 Analytics

Adicione tracking de eventos:

// Google Analytics
gtag('event', 'chat_message_sent', {
  user_id: CONFIG.userId,
  session_id: CONFIG.sessionId,
});

🐛 Troubleshooting

CORS Error

Verifique: 1. WEBCHAT_ALLOWED_ORIGINS no .env 2. Frontend usando HTTPS se backend usar HTTPS 3. Middleware CORS está ativo no FastAPI

Sessão não persiste

Verifique: 1. localStorage está habilitado no browser 2. session_id está sendo salvo corretamente 3. Logs do backend mostram session_id recebido

Resposta lenta

WebChat é síncrono e aguarda toda a resposta do agente. Para melhor UX: 1. Implemente timeout no frontend 2. Mostre typing indicator 3. Considere streaming (requer mudanças maiores)

📚 Próximos Passos

  • [ ] Adicionar suporte a imagens/arquivos
  • [ ] Implementar streaming de respostas (SSE)
  • [ ] Widget embeddable (iframe)
  • [ ] Temas customizáveis
  • [ ] Multi-idioma

Pronto para produção! 🚀

Para mais informações, consulte: - Messaging Architecture - Messaging Examples