feat(cms): implement complete SysSiteCMS content management system

Initial implementation of the HomeSysPanel CMS built with Laravel 13.0.
Includes complete admin panel, content management, user authentication,
modular architecture for products/services/chatbot, and marketing tools.

Key features:
- User authentication with email verification and password reset
- Role-based access control (Admin/User with granular permissions)
- Content management (pages, news, FAQ, downloads, galleries, banners)
- Contact management system with subjects and responses
- Marketing tools (email campaigns, visitor analytics, social sharing)
- Modular architecture (Products, Services, Chatbot modules)
- AI Chatbot integration with OpenRouter.ai and LlamaCpp support
- Visitor tracking with geolocation
- SEO optimization with meta tags and Open Graph
- Responsive design with Bootstrap 5
- Dark mode support
- Maintenance mode
- Feature request system
This commit is contained in:
marco
2026-09-07 19:06:53 -03:00
commit 38295e7e88
279 changed files with 33691 additions and 0 deletions
@@ -0,0 +1,126 @@
@extends('layouts.admin')
@section('title', 'Chatbot - Conversa com ' . $conversation->visitor_name)
@section('content')
<div class="container-fluid px-4">
<!-- Page Heading -->
<div class="d-sm-flex align-items-center justify-content-between mb-4">
<h1 class="h3 text-gray-800">
<i class="bi bi-robot me-2"></i>Conversa com {{ $conversation->visitor_name }}
</h1>
<a href="{{ route('admin.chatbot.conversations') }}" class="btn btn-secondary btn-sm">
<i class="bi bi-arrow-left me-1"></i>Voltar
</a>
</div>
<!-- Visitor Info Card -->
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="bi bi-person me-1"></i>Dados do Visitante
</h6>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-3">
<p class="mb-1 text-muted small">Nome</p>
<p class="font-weight-bold">{{ $conversation->visitor_name }}</p>
</div>
<div class="col-md-3">
<p class="mb-1 text-muted small">Telefone</p>
<p>
@if($conversation->visitor_phone)
<a href="tel:{{ $conversation->visitor_phone }}">{{ $conversation->visitor_phone }}</a>
<a href="https://wa.me/55{{ preg_replace('/\D/', '', $conversation->visitor_phone) }}" target="_blank" class="btn btn-sm btn-success ml-2">
<i class="bi bi-whatsapp me-1"></i>WhatsApp
</a>
@else
<span class="text-muted">—</span>
@endif
</p>
</div>
<div class="col-md-3">
<p class="mb-1 text-muted small">Cidade</p>
<p>{{ $conversation->visitor_city ?? '—' }}</p>
</div>
<div class="col-md-3">
<p class="mb-1 text-muted small">Status</p>
<p>
@if($conversation->status === 'active')
<span class="badge badge-success">
<i class="bi bi-chat-dots me-1"></i>Ativa
</span>
@else
<span class="badge badge-secondary">
<i class="bi bi-check-circle me-1"></i>Finalizada
</span>
@if($conversation->ended_at)
<br><small class="text-muted">{{ $conversation->ended_at->format('d/m/Y H:i:s') }}</small>
@endif
@endif
</p>
</div>
</div>
<div class="row">
<div class="col-md-3">
<p class="mb-1 text-muted small">Data de Início</p>
<p>{{ $conversation->created_at->format('d/m/Y H:i:s') }}</p>
</div>
<div class="col-md-3">
<p class="mb-1 text-muted small">Total de Mensagens</p>
<p>{{ $conversation->messages->count() }}</p>
</div>
<div class="col-md-6">
<p class="mb-1 text-muted small">Avaliação do Atendimento</p>
<p>
@if($conversation->rating)
<span style="color: #f5a623; font-size: 1.3rem; letter-spacing: 2px;">
{{ str_repeat('★', $conversation->rating) }}{{ str_repeat('☆', 5 - $conversation->rating) }}
</span>
<span class="ml-2">({{ $conversation->rating }}/5)</span>
@if($conversation->rating_comment)
<br><span class="text-muted"><i class="bi bi-chat-quote me-1"></i>"{{ $conversation->rating_comment }}"</span>
@endif
@else
<span class="text-muted">Não avaliada</span>
@endif
</p>
</div>
</div>
</div>
</div>
<!-- Chat Messages -->
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="bi bi-chat-dots me-1"></i>Histórico de Mensagens
</h6>
</div>
<div class="card-body p-0">
<div class="p-3" style="max-height: 500px; overflow-y: auto;">
@forelse($conversation->messages as $message)
<div class="d-flex mb-3 {{ $message->role === 'user' ? 'justify-content-end' : '' }}">
<div class="{{ $message->role === 'user' ? 'bg-primary text-white' : 'bg-light' }} rounded p-3" style="max-width: 75%;">
<div class="small font-weight-bold mb-1 {{ $message->role === 'user' ? 'text-white-50' : 'text-muted' }}">
@if($message->role === 'user')
<i class="bi bi-person me-1"></i>{{ $conversation->visitor_name }}
@elseif($message->role === 'assistant')
<i class="bi bi-robot me-1"></i>Assistente
@else
<i class="bi bi-gear me-1"></i>Sistema
@endif
<span class="ml-2" style="font-size: 0.7rem;">{{ $message->created_at->format('H:i') }}</span>
</div>
<div style="white-space: pre-wrap;">{{ $message->content }}</div>
</div>
</div>
@empty
<p class="text-center text-muted py-4">Nenhuma mensagem nesta conversa.</p>
@endforelse
</div>
</div>
</div>
</div>
@endsection
@@ -0,0 +1,139 @@
@extends('layouts.admin')
@section('title', 'Chatbot - Conversas')
@section('content')
<div class="container-fluid px-4">
<!-- Page Heading -->
<div class="d-sm-flex align-items-center justify-content-between mb-4">
<h1 class="h3 text-gray-800">
<i class="bi bi-robot me-2"></i>Chatbot - Conversas
</h1>
<div class="d-flex gap-2">
<a href="{{ route('admin.chatbot.index') }}" class="btn btn-secondary btn-sm">
<i class="bi bi-book me-1"></i>Base de Conhecimento
</a>
<a href="{{ route('admin.chatbot.settings') }}" class="btn btn-secondary btn-sm">
<i class="bi bi-gear me-1"></i>Configurações
</a>
</div>
</div>
@if(session('success'))
<div class="alert alert-success alert-dismissible fade show" role="alert">
{{ session('success') }}
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
@endif
<!-- Conversations Table -->
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">Contatos Recebidos ({{ $conversations->total() }} conversas)</h6>
</div>
<div class="card-body">
@if($conversations->count() > 0)
<div class="table-responsive">
<table class="table table-bordered table-hover" width="100%" cellspacing="0">
<thead>
<tr>
<th>Nome</th>
<th>Telefone</th>
<th>Cidade</th>
<th>Status</th>
<th>Avaliação</th>
<th>Mensagens</th>
<th>Data</th>
<th width="140">Ações</th>
</tr>
</thead>
<tbody>
@foreach($conversations as $conv)
<tr class="{{ $conv->status === 'ended' ? '' : 'font-weight-bold' }}">
<td>
<i class="bi bi-person me-1 text-primary"></i>{{ $conv->visitor_name }}
</td>
<td>
@if($conv->visitor_phone)
<a href="tel:{{ $conv->visitor_phone }}">
<i class="bi bi-telephone me-1"></i>{{ $conv->visitor_phone }}
</a>
@else
<span class="text-muted">—</span>
@endif
</td>
<td>
@if($conv->visitor_city)
<i class="bi bi-geo-alt me-1"></i>{{ $conv->visitor_city }}
@else
<span class="text-muted">—</span>
@endif
</td>
<td>
@if($conv->status === 'active')
<span class="badge badge-success">
<i class="bi bi-chat-dots me-1"></i>Ativa
</span>
@else
<span class="badge badge-secondary">
<i class="bi bi-check-circle me-1"></i>Finalizada
</span>
@endif
</td>
<td>
@if($conv->rating)
<span style="color: #f5a623; font-size: 1.1rem; letter-spacing: 2px;">
{{ str_repeat('★', $conv->rating) }}{{ str_repeat('☆', 5 - $conv->rating) }}
</span>
@if($conv->rating_comment)
<br><small class="text-muted" title="{{ $conv->rating_comment }}">
<i class="bi bi-chat-quote me-1"></i>{{ Str::limit($conv->rating_comment, 40) }}
</small>
@endif
@else
<span class="text-muted">—</span>
@endif
</td>
<td>
<span class="badge badge-info">{{ $conv->messages->count() }} msgs</span>
</td>
<td>{{ $conv->created_at->format('d/m/Y H:i') }}</td>
<td>
<a href="{{ route('admin.chatbot.conversation.show', $conv) }}" class="btn btn-sm btn-outline-primary" title="Ver conversa">
<i class="bi bi-eye"></i>
</a>
@if($conv->visitor_phone)
<a href="https://wa.me/55{{ preg_replace('/\D/', '', $conv->visitor_phone) }}" target="_blank" class="btn btn-sm btn-outline-success" title="WhatsApp">
<i class="bi bi-whatsapp"></i>
</a>
@endif
<form action="{{ route('admin.chatbot.conversation.destroy', $conv) }}" method="POST" class="d-inline" onsubmit="return confirm('Tem certeza que deseja excluir esta conversa?');">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-sm btn-outline-danger" title="Excluir">
<i class="bi bi-trash"></i>
</button>
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
<div class="d-flex justify-content-center">
{{ $conversations->withQueryString()->links() }}
</div>
@else
<div class="text-center py-5">
<i class="bi bi-chat-left-text text-muted" style="font-size: 3rem;"></i>
<h5 class="mt-3 text-muted">Nenhuma conversa registrada</h5>
<p class="text-muted">As conversas aparecerão aqui quando os visitantes iniciarem um chat.</p>
</div>
@endif
</div>
</div>
</div>
@endsection
+97
View File
@@ -0,0 +1,97 @@
@extends('layouts.admin')
@section('title', 'Chatbot - Nova Informação')
@section('content')
<div class="container-fluid px-4">
<!-- Page Heading -->
<div class="d-sm-flex align-items-center justify-content-between mb-4">
<h1 class="h3 text-gray-800">
<i class="bi bi-robot me-2"></i>Nova Informação para o Chatbot
</h1>
<a href="{{ route('admin.chatbot.index') }}" class="btn btn-secondary btn-sm">
<i class="bi bi-arrow-left me-1"></i>Voltar
</a>
</div>
@if($errors->any())
<div class="alert alert-danger">
<ul class="mb-0">
@foreach($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<div class="card shadow">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">Dados da Informação</h6>
</div>
<div class="card-body">
<form action="{{ route('admin.chatbot.store') }}" method="POST">
@csrf
<div class="form-group row">
<label for="title" class="col-sm-2 col-form-label">Título <span class="text-danger">*</span></label>
<div class="col-sm-10">
<input type="text" class="form-control" id="title" name="title"
value="{{ old('title') }}" placeholder="Ex: Horário de funcionamento" required>
<small class="text-muted">Um título descritivo para identificar esta informação.</small>
</div>
</div>
<div class="form-group row">
<label for="category" class="col-sm-2 col-form-label">Categoria <span class="text-danger">*</span></label>
<div class="col-sm-10">
<select class="form-control" id="category" name="category" required>
@foreach($categories as $value => $label)
<option value="{{ $value }}" {{ old('category') == $value ? 'selected' : '' }}>
{{ $label }}
</option>
@endforeach
</select>
</div>
</div>
<div class="form-group row">
<label for="content" class="col-sm-2 col-form-label">Conteúdo <span class="text-danger">*</span></label>
<div class="col-sm-10">
<textarea class="form-control" id="content" name="content" rows="6" required
placeholder="Ex: Funcionamos de segunda a sexta, das 8h às 18h, e aos sábados das 8h às 12h.">{{ old('content') }}</textarea>
<small class="text-muted">Descreva a informação em detalhes. Quanto mais detalhada, melhor será a resposta do assistente.</small>
</div>
</div>
<div class="form-group row">
<label for="restrictions" class="col-sm-2 col-form-label">Restrições</label>
<div class="col-sm-10">
<textarea class="form-control" id="restrictions" name="restrictions" rows="3"
placeholder="Ex: Não informar valores exatos de serviços, apenas faixas de preço.">{{ old('restrictions') }}</textarea>
<small class="text-muted">O que a IA <strong>NÃO</strong> deve fazer ou informar sobre este tópico. Deixe vazio se não houver restrições.</small>
</div>
</div>
<div class="form-group row">
<div class="col-sm-10 offset-sm-2">
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="active" name="active"
{{ old('active', true) ? 'checked' : '' }}>
<label class="custom-control-label" for="active">Informação ativa (visível para o assistente)</label>
</div>
</div>
</div>
<div class="form-group row">
<div class="col-sm-10 offset-sm-2">
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-circle me-1"></i>Salvar Informação
</button>
<a href="{{ route('admin.chatbot.index') }}" class="btn btn-secondary ml-2">Cancelar</a>
</div>
</div>
</form>
</div>
</div>
</div>
@endsection
+95
View File
@@ -0,0 +1,95 @@
@extends('layouts.admin')
@section('title', 'Chatbot - Editar Informação')
@section('content')
<div class="container-fluid px-4">
<!-- Page Heading -->
<div class="d-sm-flex align-items-center justify-content-between mb-4">
<h1 class="h3 text-gray-800">
<i class="bi bi-robot me-2"></i>Editar Informação do Chatbot
</h1>
<a href="{{ route('admin.chatbot.index') }}" class="btn btn-secondary btn-sm">
<i class="bi bi-arrow-left me-1"></i>Voltar
</a>
</div>
@if($errors->any())
<div class="alert alert-danger">
<ul class="mb-0">
@foreach($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<div class="card shadow">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">Editar: {{ $entry->title }}</h6>
</div>
<div class="card-body">
<form action="{{ route('admin.chatbot.update', $entry) }}" method="POST">
@csrf
@method('PUT')
<div class="form-group row">
<label for="title" class="col-sm-2 col-form-label">Título <span class="text-danger">*</span></label>
<div class="col-sm-10">
<input type="text" class="form-control" id="title" name="title"
value="{{ old('title', $entry->title) }}" required>
</div>
</div>
<div class="form-group row">
<label for="category" class="col-sm-2 col-form-label">Categoria <span class="text-danger">*</span></label>
<div class="col-sm-10">
<select class="form-control" id="category" name="category" required>
@foreach($categories as $value => $label)
<option value="{{ $value }}" {{ old('category', $entry->category) == $value ? 'selected' : '' }}>
{{ $label }}
</option>
@endforeach
</select>
</div>
</div>
<div class="form-group row">
<label for="content" class="col-sm-2 col-form-label">Conteúdo <span class="text-danger">*</span></label>
<div class="col-sm-10">
<textarea class="form-control" id="content" name="content" rows="6" required>{{ old('content', $entry->content) }}</textarea>
</div>
</div>
<div class="form-group row">
<label for="restrictions" class="col-sm-2 col-form-label">Restrições</label>
<div class="col-sm-10">
<textarea class="form-control" id="restrictions" name="restrictions" rows="3"
placeholder="Ex: Não informar valores exatos de serviços, apenas faixas de preço.">{{ old('restrictions', $entry->restrictions) }}</textarea>
<small class="text-muted">O que a IA <strong>NÃO</strong> deve fazer ou informar sobre este tópico. Deixe vazio se não houver restrições.</small>
</div>
</div>
<div class="form-group row">
<div class="col-sm-10 offset-sm-2">
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="active" name="active"
{{ old('active', $entry->active) ? 'checked' : '' }}>
<label class="custom-control-label" for="active">Informação ativa (visível para o assistente)</label>
</div>
</div>
</div>
<div class="form-group row">
<div class="col-sm-10 offset-sm-2">
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-circle me-1"></i>Atualizar Informação
</button>
<a href="{{ route('admin.chatbot.index') }}" class="btn btn-secondary ml-2">Cancelar</a>
</div>
</div>
</form>
</div>
</div>
</div>
@endsection
+126
View File
@@ -0,0 +1,126 @@
@extends('layouts.admin')
@section('title', 'Chatbot - Base de Conhecimento')
@section('content')
<div class="container-fluid px-4">
<!-- Page Heading -->
<div class="d-sm-flex align-items-center justify-content-between mb-4">
<h1 class="h3 text-gray-800">
<i class="bi bi-robot me-2"></i>Chatbot - Base de Conhecimento
</h1>
<div class="d-flex gap-2">
<a href="{{ route('admin.chatbot.settings') }}" class="btn btn-secondary btn-sm">
<i class="bi bi-gear me-1"></i>Configurações
</a>
<a href="{{ route('admin.chatbot.create') }}" class="btn btn-primary btn-sm">
<i class="bi bi-plus-circle me-1"></i>Nova Informação
</a>
</div>
</div>
@if(session('success'))
<div class="alert alert-success alert-dismissible fade show" role="alert">
{{ session('success') }}
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
@endif
<!-- Info Card -->
<div class="card shadow mb-4">
<div class="card-header py-3 d-flex align-items-center">
<h6 class="m-0 font-weight-bold text-primary">Informações do Chatbot</h6>
</div>
<div class="card-body">
<p class="text-muted mb-0">
<i class="bi bi-info-circle me-1"></i>
Adicione informações sobre a empresa, produtos e serviços para que o assistente virtual possa responder aos visitantes.
Quanto mais detalhada for a base de conhecimento, melhores serão as respostas.
</p>
</div>
</div>
<!-- Knowledge Base Table -->
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">Base de Conhecimento ({{ $knowledgeEntries->total() }} entradas)</h6>
</div>
<div class="card-body">
@if($knowledgeEntries->count() > 0)
<div class="table-responsive">
<table class="table table-bordered table-hover" width="100%" cellspacing="0">
<thead>
<tr>
<th>Título</th>
<th>Categoria</th>
<th>Conteúdo</th>
<th>Restrições</th>
<th>Status</th>
<th width="120">Ações</th>
</tr>
</thead>
<tbody>
@foreach($knowledgeEntries as $entry)
<tr>
<td class="font-weight-bold">{{ $entry->title }}</td>
<td>
<span class="badge badge-info">
{{ $categories[$entry->category] ?? $entry->category }}
</span>
</td>
<td class="text-truncate" style="max-width: 250px;">
{{ Str::limit($entry->content, 80) }}
</td>
<td class="text-truncate" style="max-width: 200px;">
@if($entry->restrictions)
<span class="text-warning" title="{{ $entry->restrictions }}">
<i class="bi bi-exclamation-triangle me-1"></i>{{ Str::limit($entry->restrictions, 50) }}
</span>
@else
<span class="text-muted">—</span>
@endif
</td>
<td>
@if($entry->active)
<span class="badge badge-success">Ativo</span>
@else
<span class="badge badge-secondary">Inativo</span>
@endif
</td>
<td>
<a href="{{ route('admin.chatbot.edit', $entry) }}" class="btn btn-sm btn-outline-primary" title="Editar">
<i class="bi bi-pencil"></i>
</a>
<form action="{{ route('admin.chatbot.destroy', $entry) }}" method="POST" class="d-inline" onsubmit="return confirm('Tem certeza que deseja excluir esta informação?');">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-sm btn-outline-danger" title="Excluir">
<i class="bi bi-trash"></i>
</button>
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
<div class="d-flex justify-content-center">
{{ $knowledgeEntries->withQueryString()->links() }}
</div>
@else
<div class="text-center py-5">
<i class="bi bi-robot text-muted" style="font-size: 3rem;"></i>
<h5 class="mt-3 text-muted">Nenhuma informação cadastrada</h5>
<p class="text-muted">Adicione informações sobre a empresa para o assistente virtual.</p>
<a href="{{ route('admin.chatbot.create') }}" class="btn btn-primary btn-sm">
<i class="bi bi-plus-circle me-1"></i>Adicionar Primeira Informação
</a>
</div>
@endif
</div>
</div>
</div>
@endsection
@@ -0,0 +1,119 @@
@extends('layouts.admin')
@section('title', 'Chatbot - Nova Regra')
@section('content')
<div class="container-fluid px-4">
<!-- Page Heading -->
<div class="d-sm-flex align-items-center justify-content-between mb-4">
<h1 class="h3 text-gray-800">
<i class="bi bi-shield-lock me-2"></i>Nova Regra para a IA
</h1>
<a href="{{ route('admin.chatbot.rules.index') }}" class="btn btn-secondary btn-sm">
<i class="bi bi-arrow-left me-1"></i>Voltar
</a>
</div>
@if($errors->any())
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<strong><i class="bi bi-exclamation-triangle me-1"></i>Erro(s):</strong>
<ul class="mb-0">
@foreach($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
<button type="button" class="close" data-dismiss="alert" aria-label="Fechar">
<span aria-hidden="true">&times;</span>
</button>
</div>
@endif
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">Dados da Regra</h6>
</div>
<div class="card-body">
<form action="{{ route('admin.chatbot.rules.store') }}" method="POST">
@csrf
<div class="form-group row">
<label for="title" class="col-sm-3 col-form-label">Título <span class="text-danger">*</span></label>
<div class="col-sm-9">
<input type="text" class="form-control" id="title" name="title"
value="{{ old('title') }}" required maxlength="255"
placeholder="Ex: Sempre oferecer WhatsApp para agendamentos">
<small class="text-muted">Nome curto para identificar a regra na listagem.</small>
</div>
</div>
<div class="form-group row">
<label for="content" class="col-sm-3 col-form-label">Conteúdo da Regra <span class="text-danger">*</span></label>
<div class="col-sm-9">
<textarea class="form-control" id="content" name="content" rows="4" required maxlength="2000"
placeholder="Ex: Sempre que o visitante demonstrar interesse em agendar um serviço, ofereça o WhatsApp (48) 99999-9999 como canal preferencial de agendamento.">{{ old('content') }}</textarea>
<small class="text-muted">A instrução completa que será enviada à IA. Máximo de 2000 caracteres.</small>
</div>
</div>
<div class="form-group row">
<label for="order" class="col-sm-3 col-form-label">Ordem</label>
<div class="col-sm-9">
<input type="number" class="form-control" id="order" name="order"
value="{{ old('order', 0) }}" min="0" max="9999">
<small class="text-muted">Define a prioridade da regra (menor número = maior prioridade). Padrão: 0.</small>
</div>
</div>
<div class="form-group row">
<label class="col-sm-3 col-form-label">Status</label>
<div class="col-sm-9">
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="active" name="active"
{{ old('active', true) ? 'checked' : '' }}>
<label class="custom-control-label" for="active">
<strong>Regra Ativa</strong> — Quando marcada, a regra será seguida pela IA nas conversas.
</label>
</div>
</div>
</div>
<div class="form-group row mb-0">
<div class="col-sm-9 offset-sm-3">
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-circle me-1"></i>Salvar Regra
</button>
<a href="{{ route('admin.chatbot.rules.index') }}" class="btn btn-secondary ml-2">Cancelar</a>
</div>
</div>
</form>
</div>
</div>
<!-- Examples Card -->
<div class="card border-info mb-4">
<div class="card-header bg-info text-white py-3">
<h6 class="m-0 font-weight-bold"><i class="bi bi-lightbulb me-1"></i>Exemplos de Regras Úteis</h6>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<p class="small mb-1"><strong>🎯 Direcionar para contato humano:</strong></p>
<p class="small text-muted">"Sempre que o visitante tiver uma dúvida complexa ou reclamação, sugira entrar em contato pelo telefone (XX) XXXX-XXXX."</p>
</div>
<div class="col-md-6">
<p class="small mb-1"><strong>💬 Tom de voz:</strong></p>
<p class="small text-muted">"Use sempre linguagem formal e profissional. Trate o visitante por 'você' mas evite gírias."</p>
</div>
<div class="col-md-6">
<p class="small mb-1"><strong>🚫 Proibir descontos:</strong></p>
<p class="small text-muted">"NUNCA ofereça descontos, promova valores ou negocie preços. Indique que valores são praticados apenas pela equipe comercial."</p>
</div>
<div class="col-md-6">
<p class="small mb-1"><strong>📍 Atendimento regional:</strong></p>
<p class="small text-muted">"Atendemos apenas as cidades de Florianópolis, São José e Biguaçu. Para outras regiões, informe que não há cobertura."</p>
</div>
</div>
</div>
</div>
</div>
@endsection
@@ -0,0 +1,91 @@
@extends('layouts.admin')
@section('title', 'Chatbot - Editar Regra')
@section('content')
<div class="container-fluid px-4">
<!-- Page Heading -->
<div class="d-sm-flex align-items-center justify-content-between mb-4">
<h1 class="h3 text-gray-800">
<i class="bi bi-shield-lock me-2"></i>Editar Regra da IA
</h1>
<a href="{{ route('admin.chatbot.rules.index') }}" class="btn btn-secondary btn-sm">
<i class="bi bi-arrow-left me-1"></i>Voltar
</a>
</div>
@if($errors->any())
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<strong><i class="bi bi-exclamation-triangle me-1"></i>Erro(s):</strong>
<ul class="mb-0">
@foreach($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
<button type="button" class="close" data-dismiss="alert" aria-label="Fechar">
<span aria-hidden="true">&times;</span>
</button>
</div>
@endif
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">Dados da Regra</h6>
</div>
<div class="card-body">
<form action="{{ route('admin.chatbot.rules.update', $rule) }}" method="POST">
@csrf
@method('PUT')
<div class="form-group row">
<label for="title" class="col-sm-3 col-form-label">Título <span class="text-danger">*</span></label>
<div class="col-sm-9">
<input type="text" class="form-control" id="title" name="title"
value="{{ old('title', $rule->title) }}" required maxlength="255">
<small class="text-muted">Nome curto para identificar a regra na listagem.</small>
</div>
</div>
<div class="form-group row">
<label for="content" class="col-sm-3 col-form-label">Conteúdo da Regra <span class="text-danger">*</span></label>
<div class="col-sm-9">
<textarea class="form-control" id="content" name="content" rows="4" required maxlength="2000">{{ old('content', $rule->content) }}</textarea>
<small class="text-muted">A instrução completa que será enviada à IA. Máximo de 2000 caracteres.</small>
</div>
</div>
<div class="form-group row">
<label for="order" class="col-sm-3 col-form-label">Ordem</label>
<div class="col-sm-9">
<input type="number" class="form-control" id="order" name="order"
value="{{ old('order', $rule->order) }}" min="0" max="9999">
<small class="text-muted">Define a prioridade da regra (menor número = maior prioridade).</small>
</div>
</div>
<div class="form-group row">
<label class="col-sm-3 col-form-label">Status</label>
<div class="col-sm-9">
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="active" name="active"
{{ old('active', $rule->active) ? 'checked' : '' }}>
<label class="custom-control-label" for="active">
<strong>Regra Ativa</strong> — Quando marcada, a regra será seguida pela IA nas conversas.
</label>
</div>
</div>
</div>
<div class="form-group row mb-0">
<div class="col-sm-9 offset-sm-3">
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-circle me-1"></i>Salvar Alterações
</button>
<a href="{{ route('admin.chatbot.rules.index') }}" class="btn btn-secondary ml-2">Cancelar</a>
</div>
</div>
</form>
</div>
</div>
</div>
@endsection
@@ -0,0 +1,110 @@
@extends('layouts.admin')
@section('title', 'Chatbot - Regras da IA')
@section('content')
<div class="container-fluid px-4">
<!-- Page Heading -->
<div class="d-sm-flex align-items-center justify-content-between mb-4">
<h1 class="h3 text-gray-800">
<i class="bi bi-shield-lock me-2"></i>Chatbot - Regras da IA
</h1>
<a href="{{ route('admin.chatbot.rules.create') }}" class="btn btn-primary btn-sm">
<i class="bi bi-plus-circle me-1"></i>Nova Regra
</a>
</div>
<!-- Info Card -->
<div class="card border-info mb-4">
<div class="card-body">
<h6 class="card-title mb-2"><i class="bi bi-info-circle me-1 text-info"></i>Como funcionam as Regras</h6>
<p class="small text-muted mb-0">
As regras são instruções obrigatórias que a IA <strong>deve seguir</strong> em todas as respostas.
Elas são enviadas automaticamente no início do prompt do sistema, antes das informações da base de conhecimento.
Use para definir tom de voz, proibições específicas, fluxos obrigatórios ou qualquer comportamento desejado.
Regras desativadas são ignoradas. Use o campo <strong>Ordem</strong> para definir a prioridade (menor = mais importante).
</p>
</div>
</div>
@if(session('success'))
<div class="alert alert-success alert-dismissible fade show" role="alert">
<i class="bi bi-check-circle me-1"></i>{{ session('success') }}
<button type="button" class="close" data-dismiss="alert" aria-label="Fechar">
<span aria-hidden="true">&times;</span>
</button>
</div>
@endif
<!-- Rules List -->
<div class="card shadow mb-4">
<div class="card-header py-3 d-flex align-items-center">
<h6 class="m-0 font-weight-bold text-primary">Regras Configuradas</h6>
</div>
<div class="card-body p-0">
@if($rules->isEmpty())
<div class="text-center py-5">
<i class="bi bi-shield-lock" style="font-size:3rem;color:#dddfeb;"></i>
<p class="mt-3 text-muted">Nenhuma regra configurada ainda.</p>
<p class="text-muted small">Adicione regras para definir como a IA deve se comportar no atendimento.</p>
<a href="{{ route('admin.chatbot.rules.create') }}" class="btn btn-primary btn-sm">
<i class="bi bi-plus-circle me-1"></i>Adicionar Primeira Regra
</a>
</div>
@else
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="thead-light">
<tr>
<th width="60">Ordem</th>
<th>Título</th>
<th>Conteúdo da Regra</th>
<th width="100">Status</th>
<th width="140">Ações</th>
</tr>
</thead>
<tbody>
@foreach($rules as $rule)
<tr>
<td>
<span class="badge badge-secondary">{{ $rule->order }}</span>
</td>
<td>
<strong>{{ $rule->title }}</strong>
</td>
<td>
<span class="text-muted">{{ \Illuminate\Support\Str::limit($rule->content, 150) }}</span>
</td>
<td>
@if($rule->active)
<span class="badge badge-success"><i class="bi bi-check-circle"></i> Ativa</span>
@else
<span class="badge badge-secondary"><i class="bi bi-pause-circle"></i> Inativa</span>
@endif
</td>
<td>
<a href="{{ route('admin.chatbot.rules.edit', $rule) }}" class="btn btn-sm btn-outline-primary" title="Editar">
<i class="bi bi-pencil"></i>
</a>
<form action="{{ route('admin.chatbot.rules.destroy', $rule) }}" method="POST" class="d-inline" onsubmit="return confirm('Tem certeza que deseja excluir esta regra?');">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-sm btn-outline-danger" title="Excluir">
<i class="bi bi-trash"></i>
</button>
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
<div class="p-3">
{{ $rules->withQueryString()->links() }}
</div>
@endif
</div>
</div>
</div>
@endsection
@@ -0,0 +1,296 @@
@extends('layouts.admin')
@section('title', 'Chatbot - Configurações')
@section('content')
<div class="container-fluid px-4">
<!-- Page Heading -->
<div class="d-sm-flex align-items-center justify-content-between mb-4">
<h1 class="h3 text-gray-800">
<i class="bi bi-robot me-2"></i>Chatbot - Configurações
</h1>
<a href="{{ route('admin.chatbot.index') }}" class="btn btn-secondary btn-sm">
<i class="bi bi-arrow-left me-1"></i>Base de Conhecimento
</a>
</div>
@if(session('success'))
<div class="alert alert-success alert-dismissible fade show" role="alert">
{{ session('success') }}
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
@endif
@if($errors->any())
<div class="alert alert-danger">
<ul class="mb-0">
@foreach($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
@php
$totalTokens = (int) \App\Models\Setting::get('chatbot_total_tokens_used', 0);
$totalMessages = \App\Modules\Chatbot\Models\ChatbotMessage::where('role', 'assistant')->count();
$totalConversations = \App\Modules\Chatbot\Models\ChatbotConversation::whereNotNull('visitor_name')->count();
@endphp
<!-- Token Usage Stats -->
<div class="row mb-4">
<div class="col-xl-4 col-md-6 mb-4">
<div class="card border-left-primary shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-primary text-uppercase mb-1">Total de Tokens Consumidos</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">{{ number_format($totalTokens, 0, ',', '.') }}</div>
</div>
<div class="col-auto">
<i class="bi bi-cpu text-primary" style="font-size: 2rem;"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-4 col-md-6 mb-4">
<div class="card border-left-success shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-success text-uppercase mb-1">Respostas da IA</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">{{ number_format($totalMessages, 0, ',', '.') }}</div>
</div>
<div class="col-auto">
<i class="bi bi-robot text-success" style="font-size: 2rem;"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-4 col-md-6 mb-4">
<div class="card border-left-info shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-info text-uppercase mb-1">Conversas com Visitantes</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">{{ number_format($totalConversations, 0, ',', '.') }}</div>
</div>
<div class="col-auto">
<i class="bi bi-chat-dots text-info" style="font-size: 2rem;"></i>
</div>
</div>
</div>
</div>
</div>
</div>
<form action="{{ route('admin.chatbot.settings.update') }}" method="POST">
@csrf
@method('POST')
<!-- General Settings -->
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="bi bi-toggle-on me-1"></i>Ativação
</h6>
</div>
<div class="card-body">
<div class="form-group row">
<div class="col-sm-10 offset-sm-2">
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="chatbot_enabled" name="chatbot_enabled"
{{ ($settings['chatbot_enabled'] ?? 'off') === 'on' ? 'checked' : '' }}>
<label class="custom-control-label" for="chatbot_enabled">
<strong>Ativar Chatbot</strong> — Exibe o widget de chat no site público
</label>
</div>
</div>
</div>
</div>
</div>
<!-- Provider Selection -->
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="bi bi-cpu me-1"></i>Provedor de IA
</h6>
</div>
<div class="card-body">
<div class="form-group row">
<label for="chatbot_provider" class="col-sm-3 col-form-label">Provedor <span class="text-danger">*</span></label>
<div class="col-sm-9">
<select class="form-control" id="chatbot_provider" name="chatbot_provider" onchange="toggleProviderFields()">
<option value="openrouter" {{ ($settings['chatbot_provider'] ?? 'openrouter') === 'openrouter' ? 'selected' : '' }}>OpenRouter.ai (nuvem)</option>
<option value="llamacpp" {{ ($settings['chatbot_provider'] ?? 'openrouter') === 'llamacpp' ? 'selected' : '' }}>llama.cpp (local)</option>
</select>
<small class="text-muted">Escolha entre usar a API do OpenRouter.ai na nuvem ou um servidor llama.cpp local.</small>
</div>
</div>
</div>
</div>
<!-- OpenRouter Settings -->
<div class="card shadow mb-4" id="openrouter-settings">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="bi bi-key me-1"></i>API OpenRouter
</h6>
</div>
<div class="card-body">
<div class="alert alert-info">
<i class="bi bi-info-circle me-1"></i>
O Chatbot pode utilizar a API do <a href="https://openrouter.ai" target="_blank">OpenRouter.ai</a> para gerar respostas inteligentes.
Crie uma conta gratuita em openrouter.ai, obtenha sua chave de API e configure abaixo.
</div>
<div class="form-group row">
<label for="chatbot_openrouter_api_key" class="col-sm-3 col-form-label">Chave da API <span class="text-danger">*</span></label>
<div class="col-sm-9">
<input type="password" class="form-control" id="chatbot_openrouter_api_key"
name="chatbot_openrouter_api_key"
value="{{ $settings['chatbot_openrouter_api_key'] ?? '' }}"
placeholder="sk-or-v1-...">
<small class="text-muted">Sua chave de API do OpenRouter.ai. Mantenha este campo seguro.</small>
</div>
</div>
<div class="form-group row">
<label for="chatbot_openrouter_model" class="col-sm-3 col-form-label">Modelo de IA</label>
<div class="col-sm-9">
<input type="text" class="form-control" id="chatbot_openrouter_model"
name="chatbot_openrouter_model"
value="{{ $settings['chatbot_openrouter_model'] ?? 'meta-llama/llama-3.1-8b-instruct' }}"
placeholder="meta-llama/llama-3.1-8b-instruct">
<small class="text-muted">Digite o ID do modelo (ex: meta-llama/llama-3.2-1b-instruct). Verifique os modelos disponíveis no <a href="https://openrouter.ai/models" target="_blank">OpenRouter.ai</a>.</small>
</div>
</div>
</div>
</div>
<!-- llama.cpp Settings -->
<div class="card shadow mb-4" id="llamacpp-settings">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="bi bi-server me-1"></i>llama.cpp (Servidor Local)
</h6>
</div>
<div class="card-body">
<div class="alert alert-info">
<i class="bi bi-info-circle me-1"></i>
Utilize um servidor <a href="https://github.com/ggerganov/llama.cpp" target="_blank">llama.cpp</a> rodando localmente para processar as respostas.
O servidor deve estar acessível com API compatível com OpenAI (<code>/v1/chat/completions</code>).
</div>
<div class="form-group row">
<label for="chatbot_llamacpp_base_url" class="col-sm-3 col-form-label">URL Base <span class="text-danger">*</span></label>
<div class="col-sm-9">
<input type="text" class="form-control" id="chatbot_llamacpp_base_url"
name="chatbot_llamacpp_base_url"
value="{{ $settings['chatbot_llamacpp_base_url'] ?? 'http://172.17.0.1:8080' }}"
placeholder="http://172.17.0.1:8080">
<small class="text-muted">URL do servidor llama.cpp (ex: http://172.17.0.1:8080). O servidor deve expor a rota <code>/v1/chat/completions</code>.</small>
</div>
</div>
<div class="form-group row">
<label for="chatbot_llamacpp_model" class="col-sm-3 col-form-label">Nome do Modelo</label>
<div class="col-sm-9">
<input type="text" class="form-control" id="chatbot_llamacpp_model"
name="chatbot_llamacpp_model"
value="{{ $settings['chatbot_llamacpp_model'] ?? '' }}"
placeholder="opcional">
<small class="text-muted">Algumas configurações do llama.cpp exigem o nome do modelo. Deixe em branco se não for necessário.</small>
</div>
</div>
</div>
</div>
<!-- Parameters -->
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="bi bi-sliders me-1"></i>Parâmetros da IA
</h6>
</div>
<div class="card-body">
<div class="form-group row">
<label for="chatbot_max_tokens" class="col-sm-3 col-form-label">Máx. Tokens de Resposta</label>
<div class="col-sm-9">
<input type="number" class="form-control" id="chatbot_max_tokens"
name="chatbot_max_tokens" min="50" max="2000"
value="{{ $settings['chatbot_max_tokens'] ?? '500' }}">
<small class="text-muted">Controla o tamanho máximo da resposta (50-2000). Recomendado: 500.</small>
</div>
</div>
<div class="form-group row">
<label for="chatbot_temperature" class="col-sm-3 col-form-label">Temperatura</label>
<div class="col-sm-9">
<input type="number" class="form-control" id="chatbot_temperature"
name="chatbot_temperature" min="0" max="2" step="0.1"
value="{{ $settings['chatbot_temperature'] ?? '0.7' }}">
<small class="text-muted">Controla a criatividade das respostas (0.0 = preciso, 2.0 = criativo). Recomendado: 0.7.</small>
</div>
</div>
</div>
</div>
<!-- Appearance Settings -->
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">
<i class="bi bi-chat-dots me-1"></i>Aparência e Mensagens
</h6>
</div>
<div class="card-body">
<div class="form-group row">
<label for="chatbot_welcome_message" class="col-sm-3 col-form-label">Mensagem de Boas-vindas</label>
<div class="col-sm-9">
<input type="text" class="form-control" id="chatbot_welcome_message"
name="chatbot_welcome_message"
value="{{ $settings['chatbot_welcome_message'] ?? 'Olá! Como posso ajudar você hoje?' }}"
maxlength="500">
<small class="text-muted">Primeira mensagem exibida quando o visitante abre o chat.</small>
</div>
</div>
<div class="form-group row">
<label for="chatbot_custom_instructions" class="col-sm-3 col-form-label">Instruções Personalizadas</label>
<div class="col-sm-9">
<textarea class="form-control" id="chatbot_custom_instructions"
name="chatbot_custom_instructions" rows="4"
placeholder="Ex: Sempre ofereça o WhatsApp como canal de contato para agendamentos.">{{ $settings['chatbot_custom_instructions'] ?? '' }}</textarea>
<small class="text-muted">Instruções extras para o comportamento do assistente. Estas são adicionadas ao prompt do sistema.</small>
</div>
</div>
</div>
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-circle me-1"></i>Salvar Configurações
</button>
<a href="{{ route('admin.chatbot.index') }}" class="btn btn-secondary ml-2">Cancelar</a>
</div>
</form>
</div>
<script>
function toggleProviderFields() {
const provider = document.getElementById('chatbot_provider').value;
document.getElementById('openrouter-settings').style.display = provider === 'openrouter' ? 'block' : 'none';
document.getElementById('llamacpp-settings').style.display = provider === 'llamacpp' ? 'block' : 'none';
}
// Run on page load
document.addEventListener('DOMContentLoaded', function() {
toggleProviderFields();
});
</script>
@endsection
@@ -0,0 +1,948 @@
@php
$chatbotEnabled = \App\Models\Setting::get('chatbot_enabled', 'off') === 'on';
@endphp
@if($chatbotEnabled && module_enabled('chatbot'))
<!-- Chatbot Widget -->
<style>
#chatbot-toggle {
position: fixed;
bottom: 20px;
right: 30px;
width: 56px;
height: 56px;
border-radius: 50%;
background: linear-gradient(135deg, #4e73df, #2e59d9);
color: white;
border: none;
cursor: pointer;
box-shadow: 0 4px 15px rgba(78, 115, 223, 0.4);
z-index: 999;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
font-size: 1.4rem;
}
#chatbot-toggle:hover {
transform: scale(1.1);
box-shadow: 0 6px 20px rgba(78, 115, 223, 0.6);
}
#chatbot-window {
position: fixed;
bottom: 90px;
right: 30px;
width: 370px;
max-height: 480px;
border-radius: 16px;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
z-index: 998;
display: none;
flex-direction: column;
overflow: hidden;
background: #fff;
animation: chatSlideUp 0.3s ease;
}
@keyframes chatSlideUp {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
#chatbot-window.open {
display: flex;
}
.chatbot-header {
background: linear-gradient(135deg, #4e73df, #2e59d9);
color: white;
padding: 16px 20px;
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
}
.chatbot-header h6 {
margin: 0;
font-weight: 700;
font-size: 1rem;
}
.chatbot-header small {
opacity: 0.8;
font-size: 0.75rem;
}
.chatbot-header-actions {
display: flex;
align-items: center;
gap: 8px;
}
.chatbot-close {
background: none;
border: none;
color: white;
font-size: 1.2rem;
cursor: pointer;
padding: 0;
opacity: 0.8;
transition: opacity 0.2s;
}
.chatbot-close:hover {
opacity: 1;
}
.chatbot-end-btn {
background: rgba(255,255,255,0.2);
border: 1px solid rgba(255,255,255,0.4);
color: white;
font-size: 0.75rem;
cursor: pointer;
padding: 3px 10px;
border-radius: 12px;
display: flex;
align-items: center;
gap: 4px;
transition: background 0.2s;
white-space: nowrap;
}
.chatbot-end-btn:hover {
background: rgba(255,255,255,0.35);
}
/* Pre-chat form */
.chatbot-prechat {
padding: 20px;
flex: 1;
overflow-y: auto;
}
.chatbot-prechat p {
color: #666;
font-size: 0.875rem;
margin-bottom: 16px;
}
.chatbot-prechat .form-group {
margin-bottom: 12px;
}
.chatbot-prechat label {
font-size: 0.8rem;
font-weight: 600;
color: #444;
margin-bottom: 4px;
display: block;
}
.chatbot-prechat input {
width: 100%;
border: 1px solid #ddd;
border-radius: 8px;
padding: 10px 12px;
font-size: 0.875rem;
outline: none;
transition: border-color 0.2s;
box-sizing: border-box;
}
.chatbot-prechat input:focus {
border-color: #4e73df;
}
.chatbot-prechat .btn-start {
width: 100%;
background: linear-gradient(135deg, #4e73df, #2e59d9);
color: white;
border: none;
border-radius: 8px;
padding: 12px;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
transition: opacity 0.2s;
}
.chatbot-prechat .btn-start:hover {
opacity: 0.9;
}
.chatbot-prechat .btn-start:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.chatbot-prechat .error-msg {
color: #e74c3c;
font-size: 0.8rem;
margin-top: 8px;
display: none;
}
/* Chat area */
.chatbot-messages {
flex: 1;
overflow-y: auto;
padding: 16px;
max-height: 360px;
min-height: 200px;
}
.chatbot-message {
margin-bottom: 12px;
display: flex;
}
.chatbot-message.user {
justify-content: flex-end;
}
.chatbot-message .bubble {
max-width: 80%;
padding: 10px 14px;
border-radius: 16px;
font-size: 0.875rem;
line-height: 1.5;
word-wrap: break-word;
}
.chatbot-message.assistant .bubble {
background: #f0f2f5;
color: #333;
border-bottom-left-radius: 4px;
}
.chatbot-message.user .bubble {
background: #4e73df;
color: white;
border-bottom-right-radius: 4px;
}
.chatbot-typing {
display: none;
margin-bottom: 12px;
padding: 0 16px;
}
.chatbot-typing .bubble {
background: #f0f2f5;
padding: 12px 18px;
border-radius: 16px;
border-bottom-left-radius: 4px;
display: flex;
gap: 4px;
}
.chatbot-typing .dot {
width: 8px;
height: 8px;
background: #999;
border-radius: 50%;
animation: typingDot 1.4s infinite;
}
.chatbot-typing .dot:nth-child(2) { animation-delay: 0.2s; }
.chatbot-typing .dot:nth-child(3) { animation-delay: 0.4s; }
@keyframes typingDot {
0%, 60%, 100% { transform: translateY(0); opacity: 0.4; }
30% { transform: translateY(-6px); opacity: 1; }
}
.chatbot-input {
display: flex;
padding: 12px 16px;
border-top: 1px solid #e0e0e0;
background: #fafafa;
flex-shrink: 0;
}
.chatbot-input input {
flex: 1;
border: 1px solid #ddd;
border-radius: 24px;
padding: 8px 16px;
font-size: 0.875rem;
outline: none;
transition: border-color 0.2s;
}
.chatbot-input input:focus {
border-color: #4e73df;
}
.chatbot-input button {
background: #4e73df;
color: white;
border: none;
border-radius: 50%;
width: 36px;
height: 36px;
margin-left: 8px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s;
}
.chatbot-input button:hover {
background: #2e59d9;
}
.chatbot-input button:disabled {
background: #ccc;
cursor: not-allowed;
}
/* Rating form */
.chatbot-rating {
padding: 20px;
text-align: center;
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.chatbot-rating h6 {
color: #333;
font-weight: 700;
margin-bottom: 8px;
font-size: 1rem;
}
.chatbot-rating p {
color: #666;
font-size: 0.85rem;
margin-bottom: 16px;
}
.chatbot-rating .stars {
display: flex;
gap: 8px;
margin-bottom: 16px;
}
.chatbot-rating .stars .star {
font-size: 2rem;
cursor: pointer;
color: #ddd;
transition: color 0.2s, transform 0.2s;
background: none;
border: none;
padding: 0;
line-height: 1;
}
.chatbot-rating .stars .star:hover,
.chatbot-rating .stars .star.active {
color: #f5a623;
transform: scale(1.15);
}
.chatbot-rating textarea {
width: 100%;
border: 1px solid #ddd;
border-radius: 8px;
padding: 10px 12px;
font-size: 0.85rem;
outline: none;
resize: none;
height: 60px;
margin-bottom: 12px;
box-sizing: border-box;
font-family: inherit;
}
.chatbot-rating textarea:focus {
border-color: #4e73df;
}
.chatbot-rating .btn-submit-rating {
width: 100%;
background: linear-gradient(135deg, #4e73df, #2e59d9);
color: white;
border: none;
border-radius: 8px;
padding: 12px;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
transition: opacity 0.2s;
}
.chatbot-rating .btn-submit-rating:hover {
opacity: 0.9;
}
.chatbot-rating .btn-submit-rating:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.chatbot-rating .btn-skip {
background: none;
border: none;
color: #999;
font-size: 0.8rem;
cursor: pointer;
margin-top: 8px;
text-decoration: underline;
}
.chatbot-rating .btn-skip:hover {
color: #666;
}
.chatbot-rating .error-msg {
color: #e74c3c;
font-size: 0.8rem;
margin-bottom: 8px;
display: none;
}
/* Thank you */
.chatbot-thankyou {
padding: 30px 20px;
text-align: center;
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.chatbot-thankyou .icon {
font-size: 3rem;
color: #1cc88a;
margin-bottom: 12px;
}
.chatbot-thankyou h6 {
color: #333;
font-weight: 700;
margin-bottom: 8px;
}
.chatbot-thankyou p {
color: #666;
font-size: 0.85rem;
}
.chatbot-thankyou .btn-new-chat {
margin-top: 16px;
background: linear-gradient(135deg, #4e73df, #2e59d9);
color: white;
border: none;
border-radius: 8px;
padding: 10px 20px;
font-size: 0.85rem;
font-weight: 600;
cursor: pointer;
}
.chatbot-disclaimer {
padding: 6px 16px;
background: #fff8e1;
border-top: 1px solid #ffe082;
font-size: 0.7rem;
color: #8d6e00;
text-align: center;
flex-shrink: 0;
}
@media (max-width: 480px) {
#chatbot-toggle {
bottom: 15px;
right: 20px;
width: 50px;
height: 50px;
font-size: 1.2rem;
}
#chatbot-window {
width: calc(100vw - 20px);
right: 10px;
bottom: 160px;
}
}
</style>
<button id="chatbot-toggle" title="Chat com assistente virtual">
<i class="bi bi-chat-dots-fill"></i>
</button>
<div id="chatbot-window">
<div class="chatbot-header">
<div>
<h6><i class="bi bi-robot me-1"></i>Assistente Virtual</h6>
<small>Online • Respostas instantâneas</small>
</div>
<div class="chatbot-header-actions">
<button class="chatbot-end-btn" id="chatbot-end-btn" title="Finalizar conversa" style="display:none;">
<i class="bi bi-x-circle"></i> Finalizar
</button>
<button class="chatbot-close" id="chatbot-close-btn">
<i class="bi bi-chevron-down"></i>
</button>
</div>
</div>
<!-- Pre-chat Registration Form -->
<div class="chatbot-prechat" id="chatbot-prechat">
<p>Para um atendimento personalizado, por favor preencha seus dados:</p>
<div class="form-group">
<label for="chatbot-visitor-name">Nome <span style="color:#e74c3c">*</span></label>
<input type="text" id="chatbot-visitor-name" placeholder="Seu nome completo" required>
</div>
<div class="form-group">
<label for="chatbot-visitor-phone">Telefone <span style="color:#e74c3c">*</span></label>
<input type="tel" id="chatbot-visitor-phone" placeholder="(00) 00000-0000">
</div>
<div class="form-group">
<label for="chatbot-visitor-city">Cidade <span style="color:#e74c3c">*</span></label>
<input type="text" id="chatbot-visitor-city" placeholder="Sua cidade">
</div>
<div class="error-msg" id="chatbot-register-error"></div>
<button class="btn-start" id="chatbot-start-btn">
<i class="bi bi-chat-dots me-1"></i>Iniciar Conversa
</button>
</div>
<!-- Chat Area (hidden until registration) -->
<div id="chatbot-chat-area" style="display:none; flex-direction:column; flex:1; overflow:hidden;">
<div class="chatbot-messages" id="chatbot-messages">
<div class="chatbot-message assistant">
<div class="bubble" id="chatbot-welcome">Olá! Como posso ajudar você hoje?</div>
</div>
</div>
<div class="chatbot-typing" id="chatbot-typing">
<div class="bubble">
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
</div>
</div>
<div class="chatbot-input" id="chatbot-input-area">
<input type="text" id="chatbot-input" placeholder="Digite sua mensagem..." maxlength="1000" autocomplete="off">
<button id="chatbot-send-btn" title="Enviar">
<i class="bi bi-send-fill"></i>
</button>
</div>
<div class="chatbot-disclaimer">
<i class="bi bi-shield-check me-1"></i>Não compartilhe dados sensíveis (senha, cartão, CPF, outros) neste chat.
</div>
</div>
<!-- Rating Form (hidden until conversation ends) -->
<div class="chatbot-rating" id="chatbot-rating" style="display:none;">
<h6><i class="bi bi-star me-1"></i>Avalie o Atendimento</h6>
<p>Como foi sua experiência com nosso assistente?</p>
<div class="stars" id="chatbot-stars">
<button class="star" data-value="1">★</button>
<button class="star" data-value="2">★</button>
<button class="star" data-value="3">★</button>
<button class="star" data-value="4">★</button>
<button class="star" data-value="5">★</button>
</div>
<textarea id="chatbot-rating-comment" placeholder="Deixe um comentário (opcional)..."></textarea>
<div class="error-msg" id="chatbot-rating-error"></div>
<button class="btn-submit-rating" id="chatbot-submit-rating">
<i class="bi bi-check-circle me-1"></i>Enviar Avaliação
</button>
<button class="btn-skip" id="chatbot-skip-rating">Pular avaliação</button>
</div>
<!-- Thank You (hidden until rating submitted) -->
<div class="chatbot-thankyou" id="chatbot-thankyou" style="display:none;">
<div class="icon"><i class="bi bi-check-circle-fill"></i></div>
<h6>Obrigado pelo feedback!</h6>
<p>Sua opinião é muito importante para melhorarmos nosso atendimento.</p>
<button class="btn-new-chat" id="chatbot-new-chat-btn">
<i class="bi bi-chat-dots me-1"></i>Nova Conversa
</button>
</div>
</div>
<script>
(function() {
const toggle = document.getElementById('chatbot-toggle');
const window_ = document.getElementById('chatbot-window');
const closeBtn = document.getElementById('chatbot-close-btn');
const endBtn = document.getElementById('chatbot-end-btn');
const prechat = document.getElementById('chatbot-prechat');
const chatArea = document.getElementById('chatbot-chat-area');
const messagesDiv = document.getElementById('chatbot-messages');
const typingIndicator = document.getElementById('chatbot-typing');
const inputEl = document.getElementById('chatbot-input');
const inputArea = document.getElementById('chatbot-input-area');
const sendBtn = document.getElementById('chatbot-send-btn');
const welcomeEl = document.getElementById('chatbot-welcome');
const startBtn = document.getElementById('chatbot-start-btn');
const errorMsg = document.getElementById('chatbot-register-error');
const nameInput = document.getElementById('chatbot-visitor-name');
const phoneInput = document.getElementById('chatbot-visitor-phone');
const cityInput = document.getElementById('chatbot-visitor-city');
const ratingDiv = document.getElementById('chatbot-rating');
const thankyouDiv = document.getElementById('chatbot-thankyou');
const starsContainer = document.getElementById('chatbot-stars');
const ratingComment = document.getElementById('chatbot-rating-comment');
const submitRatingBtn = document.getElementById('chatbot-submit-rating');
const skipRatingBtn = document.getElementById('chatbot-skip-rating');
const ratingError = document.getElementById('chatbot-rating-error');
const newChatBtn = document.getElementById('chatbot-new-chat-btn');
// Generate or retrieve session ID
let sessionId = localStorage.getItem('chatbot_session_id');
if (!sessionId) {
sessionId = 'chat_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
localStorage.setItem('chatbot_session_id', sessionId);
}
// Check if visitor already registered
let visitorRegistered = localStorage.getItem('chatbot_visitor_registered') === 'true';
let conversationEnded = localStorage.getItem('chatbot_conversation_ended') === 'true';
let selectedRating = 0;
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '';
// Phone mask
phoneInput.addEventListener('input', function(e) {
let v = e.target.value.replace(/\D/g, '');
if (v.length > 11) v = v.slice(0, 11);
if (v.length > 6) {
v = '(' + v.slice(0,2) + ') ' + v.slice(2,7) + '-' + v.slice(7);
} else if (v.length > 2) {
v = '(' + v.slice(0,2) + ') ' + v.slice(2);
} else if (v.length > 0) {
v = '(' + v;
}
e.target.value = v;
});
// Load welcome message
fetch('/chatbot/status')
.then(r => r.json())
.then(data => {
if (data.welcome_message && welcomeEl) {
welcomeEl.textContent = data.welcome_message;
}
})
.catch(() => {});
function toggleChat() {
window_.classList.toggle('open');
if (window_.classList.contains('open')) {
if (visitorRegistered && !conversationEnded) {
inputEl.focus();
} else if (!visitorRegistered) {
nameInput.focus();
}
}
}
toggle.addEventListener('click', toggleChat);
closeBtn.addEventListener('click', function() {
window_.classList.remove('open');
});
// Show appropriate view based on state
function showChatView() {
prechat.style.display = 'none';
chatArea.style.display = 'flex';
ratingDiv.style.display = 'none';
thankyouDiv.style.display = 'none';
endBtn.style.display = 'flex';
}
function showRatingView() {
prechat.style.display = 'none';
chatArea.style.display = 'none';
ratingDiv.style.display = 'flex';
thankyouDiv.style.display = 'none';
endBtn.style.display = 'none';
}
function showThankyouView() {
prechat.style.display = 'none';
chatArea.style.display = 'none';
ratingDiv.style.display = 'none';
thankyouDiv.style.display = 'flex';
endBtn.style.display = 'none';
}
// If already registered, show chat area directly
if (visitorRegistered && !conversationEnded) {
showChatView();
} else if (conversationEnded) {
// If conversation was ended, start fresh
resetChat();
}
function resetChat() {
// Generate new session
sessionId = 'chat_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
localStorage.setItem('chatbot_session_id', sessionId);
localStorage.removeItem('chatbot_visitor_registered');
localStorage.removeItem('chatbot_conversation_ended');
visitorRegistered = false;
conversationEnded = false;
selectedRating = 0;
// Clear messages
messagesDiv.innerHTML = '<div class="chatbot-message assistant"><div class="bubble" id="chatbot-welcome">Olá! Como posso ajudar você hoje?</div></div>';
// Show pre-chat form
prechat.style.display = 'block';
chatArea.style.display = 'none';
ratingDiv.style.display = 'none';
thankyouDiv.style.display = 'none';
endBtn.style.display = 'none';
// Clear form fields
nameInput.value = '';
phoneInput.value = '';
cityInput.value = '';
ratingComment.value = '';
// Reset stars
document.querySelectorAll('#chatbot-stars .star').forEach(function(s) {
s.classList.remove('active');
});
// Reload welcome message
fetch('/chatbot/status')
.then(r => r.json())
.then(data => {
const welEl = document.getElementById('chatbot-welcome');
if (data.welcome_message && welEl) {
welEl.textContent = data.welcome_message;
}
})
.catch(() => {});
}
// Register visitor
startBtn.addEventListener('click', function() {
const name = nameInput.value.trim();
const phone = phoneInput.value.trim();
const city = cityInput.value.trim();
errorMsg.style.display = 'none';
if (!name || !phone || !city) {
errorMsg.textContent = 'Por favor, preencha todos os campos obrigatórios.';
errorMsg.style.display = 'block';
return;
}
startBtn.disabled = true;
startBtn.textContent = 'Iniciando...';
fetch('/chatbot/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfToken
},
body: JSON.stringify({
session_id: sessionId,
name: name,
phone: phone,
city: city
})
})
.then(r => r.json())
.then(data => {
if (data.success) {
localStorage.setItem('chatbot_visitor_registered', 'true');
visitorRegistered = true;
// Personalize welcome message
const firstName = name.split(' ')[0];
const welEl = document.getElementById('chatbot-welcome');
if (welEl) {
welEl.textContent = welEl.textContent.replace(
'Olá!',
'Olá, ' + firstName + '!'
);
}
// Switch to chat area
showChatView();
inputEl.focus();
} else {
errorMsg.textContent = data.message || 'Erro ao registrar. Tente novamente.';
errorMsg.style.display = 'block';
}
})
.catch(() => {
errorMsg.textContent = 'Erro de conexão. Tente novamente.';
errorMsg.style.display = 'block';
})
.finally(() => {
startBtn.disabled = false;
startBtn.innerHTML = '<i class="bi bi-chat-dots me-1"></i>Iniciar Conversa';
});
});
// Allow Enter key on pre-chat form
[nameInput, phoneInput, cityInput].forEach(function(el) {
el.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
startBtn.click();
}
});
});
function addMessage(role, content) {
const div = document.createElement('div');
div.className = 'chatbot-message ' + role;
div.innerHTML = '<div class="bubble">' + escapeHtml(content) + '</div>';
messagesDiv.appendChild(div);
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function showTyping() {
typingIndicator.style.display = 'block';
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
function hideTyping() {
typingIndicator.style.display = 'none';
}
function sendMessage() {
const message = inputEl.value.trim();
if (!message) return;
addMessage('user', message);
inputEl.value = '';
sendBtn.disabled = true;
showTyping();
fetch('/chatbot/send', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfToken
},
body: JSON.stringify({
message: message,
session_id: sessionId
})
})
.then(r => r.json())
.then(data => {
hideTyping();
if (data.success) {
addMessage('assistant', data.message);
} else {
if (data.conversation_ended) {
addMessage('assistant', 'Esta conversa foi finalizada.');
endChatLocally();
} else {
addMessage('assistant', data.message || 'Desculpe, ocorreu um erro. Tente novamente.');
}
}
})
.catch(() => {
hideTyping();
addMessage('assistant', 'Desculpe, ocorreu um erro de conexão. Tente novamente.');
})
.finally(() => {
sendBtn.disabled = false;
inputEl.focus();
});
}
sendBtn.addEventListener('click', sendMessage);
inputEl.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
sendMessage();
}
});
// End conversation
endBtn.addEventListener('click', function() {
if (!confirm('Deseja finalizar esta conversa?')) return;
fetch('/chatbot/end', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfToken
},
body: JSON.stringify({
session_id: sessionId
})
})
.then(r => r.json())
.then(data => {
if (data.success) {
endChatLocally();
}
})
.catch(() => {
// End locally even if API fails
endChatLocally();
});
});
function endChatLocally() {
conversationEnded = true;
localStorage.setItem('chatbot_conversation_ended', 'true');
showRatingView();
}
// Star rating interaction
const starBtns = document.querySelectorAll('#chatbot-stars .star');
starBtns.forEach(function(star) {
star.addEventListener('click', function() {
selectedRating = parseInt(this.getAttribute('data-value'));
starBtns.forEach(function(s) {
const val = parseInt(s.getAttribute('data-value'));
if (val <= selectedRating) {
s.classList.add('active');
} else {
s.classList.remove('active');
}
});
});
star.addEventListener('mouseenter', function() {
const hoverVal = parseInt(this.getAttribute('data-value'));
starBtns.forEach(function(s) {
const val = parseInt(s.getAttribute('data-value'));
if (val <= hoverVal) {
s.style.color = '#f5a623';
} else {
s.style.color = '#ddd';
}
});
});
star.addEventListener('mouseleave', function() {
starBtns.forEach(function(s) {
const val = parseInt(s.getAttribute('data-value'));
if (val <= selectedRating) {
s.style.color = '#f5a623';
} else {
s.style.color = '#ddd';
}
});
});
});
// Submit rating
submitRatingBtn.addEventListener('click', function() {
ratingError.style.display = 'none';
if (selectedRating === 0) {
ratingError.textContent = 'Por favor, selecione pelo menos 1 estrela.';
ratingError.style.display = 'block';
return;
}
submitRatingBtn.disabled = true;
submitRatingBtn.innerHTML = '<i class="bi bi-hourglass me-1"></i>Enviando...';
fetch('/chatbot/rate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfToken
},
body: JSON.stringify({
session_id: sessionId,
rating: selectedRating,
comment: ratingComment.value.trim()
})
})
.then(r => r.json())
.then(data => {
if (data.success) {
showThankyouView();
} else {
ratingError.textContent = data.message || 'Erro ao enviar avaliação.';
ratingError.style.display = 'block';
}
})
.catch(() => {
ratingError.textContent = 'Erro de conexão. Tente novamente.';
ratingError.style.display = 'block';
})
.finally(() => {
submitRatingBtn.disabled = false;
submitRatingBtn.innerHTML = '<i class="bi bi-check-circle me-1"></i>Enviar Avaliação';
});
});
// Skip rating
skipRatingBtn.addEventListener('click', function() {
showThankyouView();
});
// New chat button
newChatBtn.addEventListener('click', function() {
resetChat();
nameInput.focus();
});
})();
</script>
@endif