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: Sucesso400: Requisição inválida (faltando user_id ou message)500: Erro interno do servidor503: 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