mirror of
https://github.com/EvolutionAPI/evolution-api.git
synced 2025-12-18 19:32:21 -06:00
feat: adiciona painel interativo com chat e IA para análise de mensagens
- Dashboard completo com métricas em tempo real - Chat interativo com IA para consultas em linguagem natural - Análise de sentimento das mensagens - Gráficos interativos (mensagens por dia, sentimentos) - Filtros avançados por instância e data - Top contatos e timeline de mensagens - API routes para stats, mensagens, sentimento e chat - Integração com PostgreSQL via Prisma - Interface moderna com Next.js 14, TypeScript e Tailwind CSS - Documentação completa com README e QUICKSTART
This commit is contained in:
251
dashboard/components/ChatInterface.tsx
Normal file
251
dashboard/components/ChatInterface.tsx
Normal file
@@ -0,0 +1,251 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { Send, Bot, User, Sparkles, TrendingUp, MessageSquare } from 'lucide-react';
|
||||
|
||||
interface Message {
|
||||
id: number;
|
||||
role: 'user' | 'assistant';
|
||||
content: string;
|
||||
timestamp: Date;
|
||||
data?: any;
|
||||
}
|
||||
|
||||
export default function ChatInterface() {
|
||||
const [messages, setMessages] = useState<Message[]>([
|
||||
{
|
||||
id: 1,
|
||||
role: 'assistant',
|
||||
content: 'Olá! Sou seu assistente de análise de dados do WhatsApp. Posso te ajudar a:\n\n• Analisar sentimentos das mensagens\n• Identificar padrões de conversação\n• Detectar spam e mensagens suspeitas\n• Gerar relatórios personalizados\n• Responder perguntas sobre suas métricas\n\nO que você gostaria de saber?',
|
||||
timestamp: new Date(),
|
||||
}
|
||||
]);
|
||||
const [input, setInput] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const scrollToBottom = () => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
}, [messages]);
|
||||
|
||||
const suggestedQuestions = [
|
||||
{ icon: TrendingUp, text: 'Quais são os horários de pico de mensagens?' },
|
||||
{ icon: MessageSquare, text: 'Mostre-me o sentimento geral das conversas' },
|
||||
{ icon: Sparkles, text: 'Detecte padrões nas mensagens recebidas' },
|
||||
];
|
||||
|
||||
const handleSendMessage = async () => {
|
||||
if (!input.trim() || loading) return;
|
||||
|
||||
const userMessage: Message = {
|
||||
id: messages.length + 1,
|
||||
role: 'user',
|
||||
content: input,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
|
||||
setMessages(prev => [...prev, userMessage]);
|
||||
setInput('');
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
// Chamar API real do chat
|
||||
const response = await fetch('/api/chat', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
message: input,
|
||||
instanceId: null, // Pode ser configurado para filtrar por instância
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Erro ao processar mensagem');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
const assistantMessage: Message = {
|
||||
id: messages.length + 2,
|
||||
role: 'assistant',
|
||||
content: data.response,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
|
||||
setMessages(prev => [...prev, assistantMessage]);
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
console.error('Erro ao enviar mensagem:', error);
|
||||
|
||||
const errorMessage: Message = {
|
||||
id: messages.length + 2,
|
||||
role: 'assistant',
|
||||
content: '❌ Desculpe, ocorreu um erro ao processar sua mensagem. Tente novamente.',
|
||||
timestamp: new Date(),
|
||||
};
|
||||
|
||||
setMessages(prev => [...prev, errorMessage]);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSuggestedQuestion = (question: string) => {
|
||||
setInput(question);
|
||||
};
|
||||
|
||||
const handleKeyPress = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSendMessage();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* Chat Principal */}
|
||||
<div className="lg:col-span-2">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 flex flex-col h-[700px]">
|
||||
{/* Header */}
|
||||
<div className="p-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="p-2 bg-gradient-to-r from-purple-500 to-pink-500 rounded-lg">
|
||||
<Bot className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
Assistente IA
|
||||
</h2>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Análise inteligente de dados
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Messages */}
|
||||
<div className="flex-1 overflow-y-auto p-6 space-y-4">
|
||||
{messages.map((message) => (
|
||||
<div
|
||||
key={message.id}
|
||||
className={`flex items-start space-x-3 ${
|
||||
message.role === 'user' ? 'flex-row-reverse space-x-reverse' : ''
|
||||
}`}
|
||||
>
|
||||
<div className={`flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center ${
|
||||
message.role === 'user'
|
||||
? 'bg-primary-500'
|
||||
: 'bg-gradient-to-r from-purple-500 to-pink-500'
|
||||
}`}>
|
||||
{message.role === 'user' ? (
|
||||
<User className="w-5 h-5 text-white" />
|
||||
) : (
|
||||
<Bot className="w-5 h-5 text-white" />
|
||||
)}
|
||||
</div>
|
||||
<div className={`flex-1 ${message.role === 'user' ? 'flex justify-end' : ''}`}>
|
||||
<div className={`max-w-[80%] rounded-lg p-4 ${
|
||||
message.role === 'user'
|
||||
? 'bg-primary-500 text-white'
|
||||
: 'bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-white'
|
||||
}`}>
|
||||
<p className="text-sm whitespace-pre-wrap">{message.content}</p>
|
||||
<span className={`text-xs mt-2 block ${
|
||||
message.role === 'user'
|
||||
? 'text-primary-100'
|
||||
: 'text-gray-500 dark:text-gray-400'
|
||||
}`}>
|
||||
{message.timestamp.toLocaleTimeString('pt-BR', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{loading && (
|
||||
<div className="flex items-start space-x-3">
|
||||
<div className="flex-shrink-0 w-8 h-8 rounded-full bg-gradient-to-r from-purple-500 to-pink-500 flex items-center justify-center">
|
||||
<Bot className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="max-w-[80%] rounded-lg p-4 bg-gray-100 dark:bg-gray-700">
|
||||
<div className="flex space-x-2">
|
||||
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce"></div>
|
||||
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '0.1s' }}></div>
|
||||
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '0.2s' }}></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
{/* Input */}
|
||||
<div className="p-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<div className="flex space-x-2">
|
||||
<input
|
||||
type="text"
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyPress={handleKeyPress}
|
||||
placeholder="Digite sua pergunta..."
|
||||
disabled={loading}
|
||||
className="flex-1 px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent dark:bg-gray-700 dark:text-white disabled:opacity-50"
|
||||
/>
|
||||
<button
|
||||
onClick={handleSendMessage}
|
||||
disabled={!input.trim() || loading}
|
||||
className="px-6 py-3 bg-primary-500 text-white rounded-lg hover:bg-primary-600 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center space-x-2"
|
||||
>
|
||||
<Send className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sugestões */}
|
||||
<div className="space-y-6">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6 border border-gray-200 dark:border-gray-700">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
||||
Perguntas Sugeridas
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
{suggestedQuestions.map((q, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => handleSuggestedQuestion(q.text)}
|
||||
className="w-full text-left p-3 rounded-lg bg-gray-50 dark:bg-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 transition-colors flex items-start space-x-3"
|
||||
>
|
||||
<q.icon className="w-5 h-5 text-primary-500 flex-shrink-0 mt-0.5" />
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">
|
||||
{q.text}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-br from-primary-500 to-emerald-500 rounded-xl shadow-sm p-6 text-white">
|
||||
<Sparkles className="w-8 h-8 mb-3" />
|
||||
<h3 className="text-lg font-semibold mb-2">
|
||||
Dica Pro
|
||||
</h3>
|
||||
<p className="text-sm text-primary-50">
|
||||
Faça perguntas específicas para obter análises mais precisas. Você pode perguntar sobre períodos, contatos, sentimentos e muito mais!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
164
dashboard/components/Dashboard.tsx
Normal file
164
dashboard/components/Dashboard.tsx
Normal file
@@ -0,0 +1,164 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Filter, Download, RefreshCw } from 'lucide-react';
|
||||
import StatsCards from './StatsCards';
|
||||
import MessageChart from './MessageChart';
|
||||
import SentimentChart from './SentimentChart';
|
||||
import TopContactsTable from './TopContactsTable';
|
||||
import MessageTimeline from './MessageTimeline';
|
||||
|
||||
export default function Dashboard() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [stats, setStats] = useState({
|
||||
totalMessages: 0,
|
||||
totalContacts: 0,
|
||||
avgResponseTime: '0min',
|
||||
activeConversations: 0,
|
||||
});
|
||||
|
||||
const [filters, setFilters] = useState({
|
||||
instanceId: '',
|
||||
startDate: '',
|
||||
endDate: '',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
fetchDashboardData();
|
||||
}, [filters]);
|
||||
|
||||
const fetchDashboardData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// Construir query string com filtros
|
||||
const params = new URLSearchParams();
|
||||
if (filters.instanceId) params.append('instanceId', filters.instanceId);
|
||||
if (filters.startDate) params.append('startDate', filters.startDate);
|
||||
if (filters.endDate) params.append('endDate', filters.endDate);
|
||||
|
||||
const response = await fetch(`/api/stats?${params.toString()}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Erro ao buscar dados');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
setStats({
|
||||
totalMessages: data.totalMessages || 0,
|
||||
totalContacts: data.totalContacts || 0,
|
||||
avgResponseTime: data.avgResponseTime || '0min',
|
||||
activeConversations: data.activeConversations || 0,
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
console.error('Erro ao buscar dados:', error);
|
||||
// Manter dados vazios em caso de erro
|
||||
setStats({
|
||||
totalMessages: 0,
|
||||
totalContacts: 0,
|
||||
avgResponseTime: '0min',
|
||||
activeConversations: 0,
|
||||
});
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleExport = () => {
|
||||
// Implementar exportação
|
||||
console.log('Exportando dados...');
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-96">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-500"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Filtros */}
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6 border border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Filter className="w-5 h-5 text-gray-500" />
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
Filtros
|
||||
</h2>
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
onClick={fetchDashboardData}
|
||||
className="px-4 py-2 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors flex items-center space-x-2"
|
||||
>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
<span>Atualizar</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={handleExport}
|
||||
className="px-4 py-2 bg-primary-500 text-white rounded-lg hover:bg-primary-600 transition-colors flex items-center space-x-2"
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
<span>Exportar</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Instância
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Todas as instâncias"
|
||||
value={filters.instanceId}
|
||||
onChange={(e) => setFilters({ ...filters, instanceId: e.target.value })}
|
||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent dark:bg-gray-700 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Data Inicial
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
value={filters.startDate}
|
||||
onChange={(e) => setFilters({ ...filters, startDate: e.target.value })}
|
||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent dark:bg-gray-700 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Data Final
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
value={filters.endDate}
|
||||
onChange={(e) => setFilters({ ...filters, endDate: e.target.value })}
|
||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent dark:bg-gray-700 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Cards de Estatísticas */}
|
||||
<StatsCards stats={stats} />
|
||||
|
||||
{/* Gráficos */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<MessageChart />
|
||||
<SentimentChart />
|
||||
</div>
|
||||
|
||||
{/* Timeline e Top Contatos */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<MessageTimeline />
|
||||
<TopContactsTable />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
71
dashboard/components/MessageChart.tsx
Normal file
71
dashboard/components/MessageChart.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
'use client';
|
||||
|
||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
|
||||
import { TrendingUp } from 'lucide-react';
|
||||
|
||||
export default function MessageChart() {
|
||||
// Dados de exemplo - substituir por dados reais da API
|
||||
const data = [
|
||||
{ name: 'Seg', enviadas: 420, recebidas: 380 },
|
||||
{ name: 'Ter', enviadas: 380, recebidas: 420 },
|
||||
{ name: 'Qua', enviadas: 520, recebidas: 480 },
|
||||
{ name: 'Qui', enviadas: 460, recebidas: 510 },
|
||||
{ name: 'Sex', enviadas: 590, recebidas: 550 },
|
||||
{ name: 'Sáb', enviadas: 320, recebidas: 280 },
|
||||
{ name: 'Dom', enviadas: 280, recebidas: 240 },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6 border border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center space-x-2">
|
||||
<TrendingUp className="w-5 h-5 text-primary-500" />
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
Mensagens por Dia
|
||||
</h3>
|
||||
</div>
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">Últimos 7 dias</span>
|
||||
</div>
|
||||
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<LineChart data={data}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#374151" opacity={0.1} />
|
||||
<XAxis
|
||||
dataKey="name"
|
||||
stroke="#6B7280"
|
||||
style={{ fontSize: '12px' }}
|
||||
/>
|
||||
<YAxis
|
||||
stroke="#6B7280"
|
||||
style={{ fontSize: '12px' }}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: '#1F2937',
|
||||
border: 'none',
|
||||
borderRadius: '8px',
|
||||
color: '#fff'
|
||||
}}
|
||||
/>
|
||||
<Legend />
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="enviadas"
|
||||
stroke="#22c55e"
|
||||
strokeWidth={2}
|
||||
dot={{ fill: '#22c55e', r: 4 }}
|
||||
activeDot={{ r: 6 }}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="recebidas"
|
||||
stroke="#3b82f6"
|
||||
strokeWidth={2}
|
||||
dot={{ fill: '#3b82f6', r: 4 }}
|
||||
activeDot={{ r: 6 }}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
118
dashboard/components/MessageTimeline.tsx
Normal file
118
dashboard/components/MessageTimeline.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
'use client';
|
||||
|
||||
import { Clock, MessageCircle } from 'lucide-react';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { ptBR } from 'date-fns/locale';
|
||||
|
||||
export default function MessageTimeline() {
|
||||
// Dados de exemplo - substituir por dados reais da API
|
||||
const recentMessages = [
|
||||
{
|
||||
id: 1,
|
||||
contact: 'João Silva',
|
||||
message: 'Olá, gostaria de saber mais sobre o produto...',
|
||||
time: new Date(Date.now() - 5 * 60 * 1000),
|
||||
sentiment: 'positive',
|
||||
type: 'received'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
contact: 'Maria Santos',
|
||||
message: 'Obrigada pelo atendimento!',
|
||||
time: new Date(Date.now() - 15 * 60 * 1000),
|
||||
sentiment: 'very_positive',
|
||||
type: 'received'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
contact: 'Pedro Oliveira',
|
||||
message: 'Ainda não recebi meu pedido',
|
||||
time: new Date(Date.now() - 30 * 60 * 1000),
|
||||
sentiment: 'negative',
|
||||
type: 'received'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
contact: 'Ana Costa',
|
||||
message: 'Qual o prazo de entrega?',
|
||||
time: new Date(Date.now() - 45 * 60 * 1000),
|
||||
sentiment: 'neutral',
|
||||
type: 'received'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
contact: 'Carlos Souza',
|
||||
message: 'Produto excelente, recomendo!',
|
||||
time: new Date(Date.now() - 60 * 60 * 1000),
|
||||
sentiment: 'very_positive',
|
||||
type: 'received'
|
||||
},
|
||||
];
|
||||
|
||||
const getSentimentColor = (sentiment: string) => {
|
||||
const colors: { [key: string]: string } = {
|
||||
very_positive: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
|
||||
positive: 'bg-green-50 text-green-700 dark:bg-green-800 dark:text-green-300',
|
||||
neutral: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200',
|
||||
negative: 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200',
|
||||
very_negative: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200',
|
||||
};
|
||||
return colors[sentiment] || colors.neutral;
|
||||
};
|
||||
|
||||
const getSentimentLabel = (sentiment: string) => {
|
||||
const labels: { [key: string]: string } = {
|
||||
very_positive: 'Muito Positivo',
|
||||
positive: 'Positivo',
|
||||
neutral: 'Neutro',
|
||||
negative: 'Negativo',
|
||||
very_negative: 'Muito Negativo',
|
||||
};
|
||||
return labels[sentiment] || 'Neutro';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6 border border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Clock className="w-5 h-5 text-primary-500" />
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
Mensagens Recentes
|
||||
</h3>
|
||||
</div>
|
||||
<button className="text-sm text-primary-500 hover:text-primary-600 font-medium">
|
||||
Ver todas
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{recentMessages.map((msg) => (
|
||||
<div
|
||||
key={msg.id}
|
||||
className="flex items-start space-x-3 p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
<div className="flex-shrink-0 mt-1">
|
||||
<MessageCircle className="w-5 h-5 text-gray-400" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{msg.contact}
|
||||
</p>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{formatDistanceToNow(msg.time, { addSuffix: true, locale: ptBR })}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-300 line-clamp-2 mb-2">
|
||||
{msg.message}
|
||||
</p>
|
||||
<span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${getSentimentColor(msg.sentiment)}`}>
|
||||
{getSentimentLabel(msg.sentiment)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
72
dashboard/components/SentimentChart.tsx
Normal file
72
dashboard/components/SentimentChart.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
'use client';
|
||||
|
||||
import { PieChart, Pie, Cell, ResponsiveContainer, Legend, Tooltip } from 'recharts';
|
||||
import { Smile } from 'lucide-react';
|
||||
|
||||
export default function SentimentChart() {
|
||||
// Dados de exemplo - substituir por dados reais da API
|
||||
const data = [
|
||||
{ name: 'Muito Positivo', value: 850, color: '#10b981' },
|
||||
{ name: 'Positivo', value: 1240, color: '#22c55e' },
|
||||
{ name: 'Neutro', value: 2130, color: '#6b7280' },
|
||||
{ name: 'Negativo', value: 420, color: '#f59e0b' },
|
||||
{ name: 'Muito Negativo', value: 183, color: '#ef4444' },
|
||||
];
|
||||
|
||||
const COLORS = data.map(item => item.color);
|
||||
|
||||
const renderCustomLabel = (entry: any) => {
|
||||
const percent = ((entry.value / data.reduce((a, b) => a + b.value, 0)) * 100).toFixed(1);
|
||||
return `${percent}%`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6 border border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Smile className="w-5 h-5 text-primary-500" />
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
Análise de Sentimento
|
||||
</h3>
|
||||
</div>
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Total: {data.reduce((a, b) => a + b.value, 0).toLocaleString('pt-BR')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={data}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
labelLine={false}
|
||||
label={renderCustomLabel}
|
||||
outerRadius={100}
|
||||
fill="#8884d8"
|
||||
dataKey="value"
|
||||
>
|
||||
{data.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: '#1F2937',
|
||||
border: 'none',
|
||||
borderRadius: '8px',
|
||||
color: '#fff'
|
||||
}}
|
||||
formatter={(value: any) => value.toLocaleString('pt-BR')}
|
||||
/>
|
||||
<Legend
|
||||
verticalAlign="bottom"
|
||||
height={36}
|
||||
iconType="circle"
|
||||
formatter={(value) => <span className="text-sm text-gray-700 dark:text-gray-300">{value}</span>}
|
||||
/>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
73
dashboard/components/StatsCards.tsx
Normal file
73
dashboard/components/StatsCards.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
'use client';
|
||||
|
||||
import { MessageSquare, Users, TrendingUp, Activity } from 'lucide-react';
|
||||
|
||||
interface StatsCardsProps {
|
||||
stats: {
|
||||
totalMessages: number;
|
||||
totalContacts: number;
|
||||
avgResponseTime: string;
|
||||
activeConversations: number;
|
||||
};
|
||||
}
|
||||
|
||||
export default function StatsCards({ stats }: StatsCardsProps) {
|
||||
const cards = [
|
||||
{
|
||||
title: 'Total de Mensagens',
|
||||
value: stats.totalMessages.toLocaleString('pt-BR'),
|
||||
icon: MessageSquare,
|
||||
color: 'from-blue-500 to-blue-600',
|
||||
change: '+12.5%',
|
||||
},
|
||||
{
|
||||
title: 'Contatos Ativos',
|
||||
value: stats.totalContacts.toLocaleString('pt-BR'),
|
||||
icon: Users,
|
||||
color: 'from-green-500 to-green-600',
|
||||
change: '+8.2%',
|
||||
},
|
||||
{
|
||||
title: 'Tempo Médio de Resposta',
|
||||
value: stats.avgResponseTime,
|
||||
icon: Activity,
|
||||
color: 'from-purple-500 to-purple-600',
|
||||
change: '-5.3%',
|
||||
},
|
||||
{
|
||||
title: 'Conversas Ativas',
|
||||
value: stats.activeConversations.toLocaleString('pt-BR'),
|
||||
icon: TrendingUp,
|
||||
color: 'from-orange-500 to-orange-600',
|
||||
change: '+15.8%',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
{cards.map((card, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6 hover:shadow-lg transition-shadow border border-gray-200 dark:border-gray-700"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className={`p-3 rounded-lg bg-gradient-to-r ${card.color}`}>
|
||||
<card.icon className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<span className={`text-sm font-medium ${
|
||||
card.change.startsWith('+') ? 'text-green-600' : 'text-red-600'
|
||||
}`}>
|
||||
{card.change}
|
||||
</span>
|
||||
</div>
|
||||
<h3 className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-1">
|
||||
{card.title}
|
||||
</h3>
|
||||
<p className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
{card.value}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
88
dashboard/components/TopContactsTable.tsx
Normal file
88
dashboard/components/TopContactsTable.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
'use client';
|
||||
|
||||
import { Users, TrendingUp, TrendingDown } from 'lucide-react';
|
||||
|
||||
export default function TopContactsTable() {
|
||||
// Dados de exemplo - substituir por dados reais da API
|
||||
const topContacts = [
|
||||
{ name: 'João Silva', phone: '+55 11 99999-1234', messages: 1245, trend: 'up', change: 12 },
|
||||
{ name: 'Maria Santos', phone: '+55 21 98888-5678', messages: 982, trend: 'up', change: 8 },
|
||||
{ name: 'Pedro Oliveira', phone: '+55 31 97777-9012', messages: 856, trend: 'down', change: -3 },
|
||||
{ name: 'Ana Costa', phone: '+55 41 96666-3456', messages: 734, trend: 'up', change: 15 },
|
||||
{ name: 'Carlos Souza', phone: '+55 51 95555-7890', messages: 628, trend: 'up', change: 5 },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6 border border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Users className="w-5 h-5 text-primary-500" />
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
Top Contatos
|
||||
</h3>
|
||||
</div>
|
||||
<button className="text-sm text-primary-500 hover:text-primary-600 font-medium">
|
||||
Ver todos
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="border-b border-gray-200 dark:border-gray-700">
|
||||
<th className="text-left py-3 px-2 text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
Contato
|
||||
</th>
|
||||
<th className="text-right py-3 px-2 text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
Mensagens
|
||||
</th>
|
||||
<th className="text-right py-3 px-2 text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
Tendência
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{topContacts.map((contact, index) => (
|
||||
<tr key={index} className="hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
|
||||
<td className="py-4 px-2">
|
||||
<div>
|
||||
<p className="font-medium text-gray-900 dark:text-white">
|
||||
{contact.name}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{contact.phone}
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-4 px-2 text-right">
|
||||
<span className="font-semibold text-gray-900 dark:text-white">
|
||||
{contact.messages.toLocaleString('pt-BR')}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-4 px-2 text-right">
|
||||
<div className="flex items-center justify-end space-x-1">
|
||||
{contact.trend === 'up' ? (
|
||||
<>
|
||||
<TrendingUp className="w-4 h-4 text-green-500" />
|
||||
<span className="text-sm font-medium text-green-500">
|
||||
+{contact.change}%
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<TrendingDown className="w-4 h-4 text-red-500" />
|
||||
<span className="text-sm font-medium text-red-500">
|
||||
{contact.change}%
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user