Em diversos cenários de atendimento web, é comum a necessidade de integrar um componente visual que simule um atendente virtual, capaz de interagir com vsiitantes através de mensagens pré-definidas ou sorteio aleatório. A implementação a seguir demonstra como construir esse módulo utilizando JavaScript moderno aliado à biblioteca jQuery, garantindo baixo acoplamento e alta capacidade de personalização.
Implementação da Classe Principal
O código foi reescrito utilizando o padrão ES6 para melhorar a organização e a reutilização. As configurações são centralizadas em um objeto de parâmetros, e a lógica de sorteio foi substituída pelo algoritmo Fisher-Yates para garantir maior imprevisibilidade na ordem das mensagens.
class AssistenteVirtual {
constructor(config) {
if (typeof jQuery === 'undefined') {
throw new Error('jQuery deve ser carregado antes da inicialização.');
}
this.$ = jQuery;
this.cfg = {
painel: config.painel || 'bot-panel',
historico: config.historico || 'chat-log',
btnAtivar: config.btnAtivar || 'toggle-bot',
btnDesativar: config.btnDesativar || 'close-bot',
campoTexto: config.campoTexto || 'msg-input',
btnEnviar: config.btnEnviar || 'send-msg',
repliesBase: config.repliesBase || ['Olá! Como posso ajudar?', 'Bem-vindo ao nosso suporte.', 'Estamos online.', 'Até logo!'],
estiloBot: config.estiloBot || 'reply-bubble',
estiloUser: config.estiloUser || 'user-bubble',
autoRespostaInicial: config.autoRespostaInicial !== false,
alternarBotoes: config.alternarBotoes !== false,
respostaFixa: config.respostaFixa || null
};
this.$painel = this.$('.' + this.cfg.painel);
this.$log = this.$('.' + this.cfg.historico);
this.$campo = this.$('.' + this.cfg.campoTexto);
this.validarDOM();
this.ancorarEventos();
this.gerarPermutacao();
this.posicao = 0;
this.tempoEspera = null;
}
validarDOM() {
const chaves = [this.cfg.painel, this.cfg.historico, this.cfg.campoTexto, this.cfg.btnAtivar];
chaves.forEach(classe => {
if (this.$('.' + classe).length === 0) {
throw new Error(`Seletor .${classe} não encontrado no DOM.`);
}
});
}
ancorarEventos() {
this.$('.' + this.cfg.btnAtivar).on('click', () => this.exibir());
this.$('.' + this.cfg.btnDesativar).on('click', () => this.ocultar());
this.$('.' + this.cfg.btnEnviar).on('click', () => this.processarEnvio());
this.$campo.on('keydown', (e) => {
if (e.key === 'Enter') this.processarEnvio();
});
}
exibir() {
this.$painel.fadeIn(300);
if (this.cfg.alternarBotoes) {
this.$('.' + this.cfg.btnAtivar).hide();
this.$('.' + this.cfg.btnDesativar).show();
}
if (this.cfg.autoRespostaInicial) {
this.dispararResposta();
}
}
ocultar() {
clearTimeout(this.tempoEspera);
this.$painel.fadeOut(200);
if (this.cfg.alternarBotoes) {
this.$('.' + this.cfg.btnDesativar).hide();
this.$('.' + this.cfg.btnAtivar).show();
}
}
processarEnvio() {
const valor = this.$campo.val().trim();
if (!valor) return;
this.adicionarAoLog(valor, this.cfg.estiloUser);
this.$campo.val('');
this.interpretarComando(valor);
}
interpretarComando(texto) {
// Ponto de extensão para lógica de negócio ou chamadas HTTP
if (texto.toLowerCase() === 'preço') {
this.dispararResposta('Nossos planos começam a partir de R$ 49,90/mês.');
return;
}
this.dispararResposta();
}
adicionarAoLog(texto, classe) {
this.$log.append(`<div class="${classe}"><p>${texto}</p></div>`);
this.atualizarScroll();
}
atualizarScroll() {
this.$log.stop().animate({ scrollTop: this.$log.prop('scrollHeight') }, 300);
}
dispararResposta(textoPersonalizado = null) {
clearTimeout(this.tempoEspera);
this.tempoEspera = setTimeout(() => {
if (textoPersonalizado) {
this.adicionarAoLog(textoPersonalizado, this.cfg.estiloBot);
return;
}
const conteudo = this.cfg.repliesBase[this.permutacao[this.posicao]];
this.adicionarAoLog(conteudo, this.cfg.estiloBot);
this.posicao = (this.posicao + 1) % this.cfg.repliesBase.length;
if (this.posicao === 0) this.gerarPermutacao();
}, 1400);
}
gerarPermutacao() {
this.permutacao = [...Array(this.cfg.repliesBase.length).keys()];
for (let i = this.permutacao.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[this.permutacao[i], this.permutacao[j]] = [this.permutacao[j], this.permutacao[i]];
}
}
}
Configuração e Instanciação
A personalização do comportamento ocorre principalmente através do objeto de configuração ou sobrescrevendo o método interpretarComando após a criação da instância. Isso permite adaptar o fluxo de conversa sem modificar o núcleo da classe.
const bot = new AssistenteVirtual({
painel: 'chat-container',
historico: 'message-stream',
btnAtivar: 'ico-support',
btnDesativar: 'x-close',
campoTexto: 'user-typing',
btnEnviar: 'submit-query',
repliesBase: [
'Olá! Estou aqui para auxiliar.',
'Descreva sua necessidade e retornarei em instantes.',
'Nossa equipe técnica está disponível.',
'Foi um prazer ajudar!'
],
estiloBot: 'atendente-msg',
estiloUser: 'visitante-msg',
autoRespostaInicial: true
});
Estrutura HTML e Estilização
O exemplo abaixo ilustra a marcação semântica e o CSS necessário para renderizar a interface. As classes utilizadas correspondem aos valores padrão definidos na configuração.
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bot de Atendimento</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<style>
body { font-family: system-ui, sans-serif; margin: 0; background: #eef2f5; }
.launcher { position: fixed; right: 24px; bottom: 24px; width: 56px; height: 56px; background: #2563eb; color: #fff; border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; box-shadow: 0 4px 12px rgba(0,0,0,0.2); font-size: 24px; }
.panel { position: fixed; right: 24px; bottom: 90px; width: 340px; height: 420px; background: #fff; border-radius: 16px; box-shadow: 0 8px 24px rgba(0,0,0,0.12); display: none; flex-direction: column; overflow: hidden; }
.top-bar { padding: 14px; background: #2563eb; color: #fff; display: flex; justify-content: space-between; align-items: center; font-weight: 600; }
.top-bar button { background: transparent; border: none; color: #fff; cursor: pointer; font-size: 18px; }
.stream { flex: 1; padding: 12px; overflow-y: auto; background: #f8fafc; }
.stream::after { content: ""; display: table; clear: both; }
.bottom-bar { display: flex; border-top: 1px solid #e2e8f0; }
.bottom-bar input { flex: 1; padding: 12px; border: none; outline: none; font-size: 14px; }
.bottom-bar button { padding: 0 16px; background: #2563eb; color: #fff; border: none; cursor: pointer; font-weight: 500; }
.atendente-msg, .visitante-msg { margin: 10px 0; }
.atendente-msg p { background: #e2e8f0; padding: 10px 14px; border-radius: 14px 14px 14px 2px; max-width: 85%; float: left; font-size: 14px; }
.visitante-msg p { background: #2563eb; color: #fff; padding: 10px 14px; border-radius: 14px 14px 2px 14px; max-width: 85%; float: right; font-size: 14px; }
</style>
</head>
<body>
<div class="launcher ico-support">💬</div>
<div class="panel chat-container">
<div class="top-bar">
<span>Suporte Online</span>
<button class="x-close">✕</button>
</div>
<div class="stream message-stream"></div>
<div class="bottom-bar">
<input type="text" class="user-typing" placeholder="Escreva aqui...">
<button class="submit-query">Enviar</button>
</div>
</div>
<script>
// Inserir classe AssistenteVirtual aqui
const bot = new AssistenteVirtual({ responderAoInicial: true });
</script>
</body>
</html>
Mecanismo de Sorteio e Extensibilidade
A geração de índices aleatórios utiliza troca de posições em vetor para evitar padrões repetitivos. Quando o ciclo de mensagens é concluído, um novo embaralhamento é executado automaticamente. Para integrações com back-end, o método interpretarComando pode ser estendido com chamadas fetch ou AJAX, garantindo que a interface permaneça responsiva enquanto aguarda respostas assíncronas.