+ Evolution Dashboard +
++ Análise Inteligente de Mensagens WhatsApp +
+diff --git a/dashboard/.env.example b/dashboard/.env.example new file mode 100644 index 00000000..3539daf3 --- /dev/null +++ b/dashboard/.env.example @@ -0,0 +1,10 @@ +# Database +DATABASE_URL="postgresql://usuario:senha@localhost:5432/evolution" + +# Redis (opcional) +REDIS_ENABLED=false +REDIS_URI="redis://localhost:6379" + +# App Config +NODE_ENV=development +NEXT_PUBLIC_API_URL=http://localhost:3000 diff --git a/dashboard/.eslintrc.json b/dashboard/.eslintrc.json new file mode 100644 index 00000000..957cd154 --- /dev/null +++ b/dashboard/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": ["next/core-web-vitals"] +} diff --git a/dashboard/.gitignore b/dashboard/.gitignore new file mode 100644 index 00000000..dca9f720 --- /dev/null +++ b/dashboard/.gitignore @@ -0,0 +1,37 @@ +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +# prisma +/prisma/migrations diff --git a/dashboard/QUICKSTART.md b/dashboard/QUICKSTART.md new file mode 100644 index 00000000..74a3483b --- /dev/null +++ b/dashboard/QUICKSTART.md @@ -0,0 +1,95 @@ +# 🚀 Guia Rápido - Evolution Dashboard + +## Instalação em 5 Minutos + +### 1. Instale as dependências +```bash +cd dashboard +npm install +``` + +### 2. Configure o banco de dados +```bash +# Copie o arquivo de exemplo +cp .env.example .env + +# Edite com suas credenciais +nano .env +``` + +Cole a string de conexão do seu PostgreSQL: +```env +DATABASE_URL="postgresql://usuario:senha@localhost:5432/evolution" +``` + +### 3. Gere o Prisma Client +```bash +npx prisma generate +``` + +### 4. Inicie o servidor +```bash +npm run dev +``` + +### 5. Acesse o painel +Abra: **http://localhost:3000** + +## ✨ Primeiro Uso + +### Dashboard +1. Clique na aba **"Dashboard"** +2. Veja suas métricas em tempo real +3. Use os filtros para refinar a análise + +### Chat IA +1. Clique na aba **"Chat IA"** +2. Faça perguntas como: + - "Quais são os horários de pico?" + - "Mostre o sentimento geral" + - "Quantas mensagens tenho hoje?" + +## 🎯 Perguntas Frequentes + +**P: Não vejo dados no dashboard** +R: Verifique se o banco Evolution API está populado e a string de conexão está correta. + +**P: Erro ao conectar no banco** +R: Confirme que o PostgreSQL está rodando: `sudo systemctl status postgresql` + +**P: Porta 3000 já está em uso** +R: Use outra porta: `PORT=3001 npm run dev` + +## 📊 Dicas + +- Use filtros por data para análises específicas +- O chat IA aprende com suas perguntas +- Exporte relatórios para análise offline +- Dark mode automático baseado no sistema + +## 🔧 Comandos Úteis + +```bash +# Desenvolvimento +npm run dev + +# Build de produção +npm run build +npm start + +# Verificar problemas +npm run lint +``` + +## 💡 Próximos Passos + +1. ✅ Explore o dashboard completo +2. ✅ Teste o chat IA com diferentes perguntas +3. ✅ Configure filtros personalizados +4. ✅ Exporte seus primeiros relatórios + +--- + +**Pronto! Seu painel está funcionando** 🎉 + +Para mais detalhes, consulte o [README.md](./README.md) diff --git a/dashboard/README.md b/dashboard/README.md new file mode 100644 index 00000000..b7bfca36 --- /dev/null +++ b/dashboard/README.md @@ -0,0 +1,338 @@ +# 📊 Evolution Dashboard + +Painel interativo com IA para análise profunda de mensagens do WhatsApp via Evolution API. + + + + + + +## ✨ Funcionalidades + +### 📈 Dashboard Completo +- **Métricas em Tempo Real**: Total de mensagens, contatos ativos, tempo médio de resposta +- **Gráficos Interativos**: Visualização de mensagens por dia com Recharts +- **Análise de Sentimento**: Distribuição de sentimentos (positivo, negativo, neutro) +- **Top Contatos**: Ranking dos contatos mais ativos +- **Timeline de Mensagens**: Visualização cronológica das mensagens recentes +- **Filtros Avançados**: Por instância, data, tipo de mensagem + +### 🤖 Chat Interativo com IA +- **Análise Inteligente**: Faça perguntas em linguagem natural sobre seus dados +- **Respostas Contextuais**: IA analisa o banco de dados em tempo real +- **Sugestões de Perguntas**: Templates prontos para análises comuns +- **Análise de Sentimento**: Detecta automaticamente o humor das mensagens +- **Insights Automáticos**: Recomendações baseadas nos padrões identificados + +### 📊 Análises Disponíveis +- ⏰ **Horários de Pico**: Identifica quando há mais mensagens +- 😊 **Análise de Sentimento**: Mede satisfação dos clientes +- 📈 **Tendências**: Detecta padrões de crescimento/declínio +- 👥 **Análise de Contatos**: Rankings e estatísticas por contato +- 🔍 **Detecção de Padrões**: Identifica temas recorrentes nas mensagens + +## 🚀 Instalação + +### Pré-requisitos +- Node.js 18+ instalado +- PostgreSQL com banco Evolution API configurado +- npm ou yarn + +### Passo a Passo + +1. **Navegue até a pasta do dashboard** +```bash +cd dashboard +``` + +2. **Instale as dependências** +```bash +npm install +``` + +3. **Configure o arquivo .env** +```bash +cp .env.example .env +``` + +Edite o arquivo `.env` com suas configurações: +```env +DATABASE_URL="postgresql://usuario:senha@localhost:5432/evolution" +NODE_ENV=development +NEXT_PUBLIC_API_URL=http://localhost:3000 +``` + +4. **Gere o Prisma Client** +```bash +npx prisma generate +``` + +5. **Execute em modo desenvolvimento** +```bash +npm run dev +``` + +6. **Acesse o painel** +Abra seu navegador em: `http://localhost:3000` + +## 📁 Estrutura do Projeto + +``` +dashboard/ +├── app/ # App directory do Next.js 14 +│ ├── api/ # API Routes +│ │ ├── stats/ # Estatísticas gerais +│ │ ├── messages/ # Busca de mensagens +│ │ ├── sentiment/ # Análise de sentimento +│ │ └── chat/ # Chat com IA +│ ├── globals.css # Estilos globais +│ ├── layout.tsx # Layout principal +│ └── page.tsx # Página inicial +├── components/ # Componentes React +│ ├── Dashboard.tsx # Dashboard principal +│ ├── ChatInterface.tsx # Interface de chat +│ ├── StatsCards.tsx # Cards de estatísticas +│ ├── MessageChart.tsx # Gráfico de mensagens +│ ├── SentimentChart.tsx # Gráfico de sentimentos +│ ├── TopContactsTable.tsx # Tabela de top contatos +│ └── MessageTimeline.tsx # Timeline de mensagens +├── lib/ # Bibliotecas e utilitários +│ └── prisma.ts # Cliente Prisma +├── prisma/ # Prisma ORM +│ └── schema.prisma # Schema do banco +├── public/ # Arquivos estáticos +├── .env.example # Exemplo de variáveis de ambiente +├── next.config.js # Configuração do Next.js +├── tailwind.config.ts # Configuração do Tailwind +├── tsconfig.json # Configuração do TypeScript +└── package.json # Dependências do projeto +``` + +## 🎯 Como Usar + +### Dashboard +1. **Acesse a aba "Dashboard"** no topo da página +2. **Aplique filtros** para refinar sua análise: + - Selecione uma instância específica + - Escolha um período de datas +3. **Visualize as métricas**: + - Cards com estatísticas principais + - Gráfico de mensagens por dia + - Distribuição de sentimentos + - Top contatos mais ativos + - Timeline de mensagens recentes +4. **Exporte os dados** clicando no botão "Exportar" + +### Chat IA +1. **Acesse a aba "Chat IA"** no topo da página +2. **Faça perguntas** sobre seus dados, como: + - "Quais são os horários de pico de mensagens?" + - "Mostre-me o sentimento geral das conversas" + - "Quantas mensagens recebi hoje?" + - "Quais são meus principais contatos?" +3. **Use as sugestões** no painel lateral para começar +4. **Receba análises detalhadas** em tempo real + +### Exemplos de Perguntas + +**Horários e Padrões:** +- "Qual o horário com mais mensagens?" +- "Em que dia da semana recebo mais mensagens?" +- "Mostre os padrões de conversação" + +**Análise de Sentimento:** +- "Como está o sentimento geral dos clientes?" +- "Quantas mensagens negativas recebi?" +- "Qual a satisfação dos clientes este mês?" + +**Estatísticas:** +- "Quantas mensagens tenho no total?" +- "Quantos contatos ativos tenho?" +- "Qual a média de mensagens por dia?" + +## 🔌 API Endpoints + +### GET /api/stats +Retorna estatísticas gerais. + +**Query Parameters:** +- `instanceId` (opcional): Filtrar por instância +- `startDate` (opcional): Data inicial (ISO 8601) +- `endDate` (opcional): Data final (ISO 8601) + +**Resposta:** +```json +{ + "totalMessages": 45823, + "totalContacts": 1253, + "avgResponseTime": "2.5min", + "activeConversations": 342, + "totalChats": 856 +} +``` + +### GET /api/messages +Retorna lista de mensagens. + +**Query Parameters:** +- `instanceId` (opcional): Filtrar por instância +- `limit` (opcional, default: 100): Limite de mensagens +- `offset` (opcional, default: 0): Offset para paginação +- `startDate` (opcional): Data inicial +- `endDate` (opcional): Data final +- `fromMe` (opcional): Filtrar por mensagens enviadas/recebidas + +### GET /api/sentiment +Analisa sentimento das mensagens. + +**Query Parameters:** +- `instanceId` (opcional): Filtrar por instância +- `limit` (opcional, default: 1000): Limite de mensagens a analisar + +**Resposta:** +```json +{ + "total": 4823, + "avgScore": "0.45", + "distribution": { + "very_positive": 850, + "positive": 1240, + "neutral": 2130, + "negative": 420, + "very_negative": 183 + } +} +``` + +### POST /api/chat +Chat interativo com IA. + +**Body:** +```json +{ + "message": "Quais são os horários de pico?", + "instanceId": "uuid-opcional" +} +``` + +**Resposta:** +```json +{ + "response": "📊 Análise de Horários de Pico...", + "timestamp": "2025-11-14T12:00:00Z" +} +``` + +## 🎨 Personalização + +### Cores do Tema +Edite `tailwind.config.ts` para personalizar as cores: + +```typescript +colors: { + primary: { + 50: '#f0fdf4', + 500: '#22c55e', + 900: '#14532d', + }, +} +``` + +### Componentes +Todos os componentes estão em `components/` e podem ser personalizados individualmente. + +### Gráficos +Os gráficos usam Recharts. Customize em: +- `components/MessageChart.tsx` +- `components/SentimentChart.tsx` + +## 🔧 Tecnologias + +- **[Next.js 14](https://nextjs.org/)** - Framework React com App Router +- **[TypeScript](https://www.typescriptlang.org/)** - Tipagem estática +- **[Tailwind CSS](https://tailwindcss.com/)** - Framework CSS utilitário +- **[Prisma](https://www.prisma.io/)** - ORM para PostgreSQL +- **[Recharts](https://recharts.org/)** - Biblioteca de gráficos +- **[Lucide Icons](https://lucide.dev/)** - Ícones modernos +- **[date-fns](https://date-fns.org/)** - Manipulação de datas +- **[Sentiment](https://www.npmjs.com/package/sentiment)** - Análise de sentimento +- **[Natural](https://www.npmjs.com/package/natural)** - NLP em JavaScript + +## 📝 Scripts Disponíveis + +```bash +npm run dev # Inicia em modo desenvolvimento +npm run build # Cria build de produção +npm start # Inicia em modo produção +npm run lint # Executa linter +``` + +## 🚀 Deploy + +### Vercel (Recomendado) +1. Faça push do código para o GitHub +2. Conecte seu repositório na [Vercel](https://vercel.com) +3. Configure as variáveis de ambiente +4. Deploy automático! + +### Docker +```bash +docker build -t evolution-dashboard . +docker run -p 3000:3000 evolution-dashboard +``` + +### Manual +```bash +npm run build +npm start +``` + +## 🔒 Segurança + +- ✅ Validação de entrada em todas as APIs +- ✅ Sanitização de dados do banco +- ✅ CORS configurado +- ✅ Rate limiting recomendado para produção +- ✅ Variáveis de ambiente para credenciais + +## 🐛 Troubleshooting + +### Erro de conexão com o banco +```bash +# Verifique se o PostgreSQL está rodando +sudo systemctl status postgresql + +# Teste a conexão +psql -h localhost -U usuario -d evolution +``` + +### Erro ao gerar Prisma Client +```bash +# Limpe e regenere +rm -rf node_modules/.prisma +npx prisma generate +``` + +### Porta 3000 já está em uso +```bash +# Use outra porta +PORT=3001 npm run dev +``` + +## 📄 Licença + +Este projeto está sob a licença MIT. Veja o arquivo LICENSE para mais detalhes. + +## 🤝 Contribuindo + +Contribuições são bem-vindas! Sinta-se à vontade para abrir issues e pull requests. + +## 📧 Suporte + +Para dúvidas e suporte: +- Abra uma issue no GitHub +- Consulte a documentação do Evolution API + +--- + +**Desenvolvido com ❤️ usando Next.js e TypeScript** diff --git a/dashboard/app/api/chat/route.ts b/dashboard/app/api/chat/route.ts new file mode 100644 index 00000000..64cebea1 --- /dev/null +++ b/dashboard/app/api/chat/route.ts @@ -0,0 +1,151 @@ +import { NextRequest, NextResponse } from 'next/server'; +import prisma from '@/lib/prisma'; +// @ts-ignore +import Sentiment from 'sentiment'; + +const sentiment = new Sentiment(); + +function extractMessageText(messageObj: any): string { + if (typeof messageObj === 'string') return messageObj; + if (!messageObj || typeof messageObj !== 'object') return ''; + + const msg = messageObj.message || messageObj; + + if (msg.conversation) return msg.conversation; + if (msg.extendedTextMessage?.text) return msg.extendedTextMessage.text; + if (msg.imageMessage?.caption) return msg.imageMessage.caption; + if (msg.videoMessage?.caption) return msg.videoMessage.caption; + + return ''; +} + +export async function POST(request: NextRequest) { + try { + const { message, instanceId } = await request.json(); + + if (!message) { + return NextResponse.json( + { error: 'Mensagem é obrigatória' }, + { status: 400 } + ); + } + + const lower = message.toLowerCase(); + + // Análise baseada em consultas ao banco + let response = ''; + + // Horários de pico + if (lower.includes('horário') || lower.includes('pico') || lower.includes('hora')) { + const where: any = {}; + if (instanceId) where.instanceId = instanceId; + + const messages = await prisma.message.findMany({ + where, + select: { messageTimestamp: true }, + }); + + // Agrupar por hora + const hourCounts: { [key: number]: number } = {}; + messages.forEach((msg: any) => { + const hour = new Date(Number(msg.messageTimestamp)).getHours(); + hourCounts[hour] = (hourCounts[hour] || 0) + 1; + }); + + // Encontrar top 3 horários + const topHours = Object.entries(hourCounts) + .sort(([, a], [, b]) => (b as number) - (a as number)) + .slice(0, 3); + + response = '📊 **Análise de Horários de Pico:**\n\n'; + topHours.forEach(([hour, count], index) => { + const percentage = ((count as number) / messages.length * 100).toFixed(1); + response += `${index + 1}. **${hour}h**: ${count} mensagens (${percentage}%)\n`; + }); + + response += `\n💡 **Total analisado**: ${messages.length.toLocaleString('pt-BR')} mensagens`; + } + + // Análise de sentimento + else if (lower.includes('sentimento') || lower.includes('humor') || lower.includes('satisfação')) { + const where: any = { fromMe: false }; + if (instanceId) where.instanceId = instanceId; + + const messages = await prisma.message.findMany({ + where, + take: 1000, + select: { message: true }, + }); + + const sentiments = { very_positive: 0, positive: 0, neutral: 0, negative: 0, very_negative: 0 }; + + messages.forEach((msg: any) => { + const text = extractMessageText(msg.message); + if (!text) return; + + const analysis = sentiment.analyze(text); + const score = analysis.score; + + if (score > 2) sentiments.very_positive++; + else if (score > 0) sentiments.positive++; + else if (score < -2) sentiments.very_negative++; + else if (score < 0) sentiments.negative++; + else sentiments.neutral++; + }); + + const total = Object.values(sentiments).reduce((a, b) => a + b, 0); + + response = '😊 **Análise de Sentimento:**\n\n'; + response += `• **Muito Positivo**: ${((sentiments.very_positive / total) * 100).toFixed(1)}% (${sentiments.very_positive} mensagens)\n`; + response += `• **Positivo**: ${((sentiments.positive / total) * 100).toFixed(1)}% (${sentiments.positive} mensagens)\n`; + response += `• **Neutro**: ${((sentiments.neutral / total) * 100).toFixed(1)}% (${sentiments.neutral} mensagens)\n`; + response += `• **Negativo**: ${((sentiments.negative / total) * 100).toFixed(1)}% (${sentiments.negative} mensagens)\n`; + response += `• **Muito Negativo**: ${((sentiments.very_negative / total) * 100).toFixed(1)}% (${sentiments.very_negative} mensagens)\n`; + + const positiveTotal = sentiments.very_positive + sentiments.positive; + const negativeTotal = sentiments.negative + sentiments.very_negative; + + response += `\n✅ **Conclusão**: ${((positiveTotal / total) * 100).toFixed(1)}% das mensagens são positivas, `; + response += `${((negativeTotal / total) * 100).toFixed(1)}% são negativas.`; + } + + // Estatísticas gerais + else if (lower.includes('total') || lower.includes('quantas') || lower.includes('estatística')) { + const where: any = {}; + if (instanceId) where.instanceId = instanceId; + + const [totalMessages, totalContacts, totalChats] = await Promise.all([ + prisma.message.count({ where }), + prisma.contact.count({ where: instanceId ? { instanceId } : {} }), + prisma.chat.count({ where: instanceId ? { instanceId } : {} }), + ]); + + response = '📈 **Estatísticas Gerais:**\n\n'; + response += `• **Total de Mensagens**: ${totalMessages.toLocaleString('pt-BR')}\n`; + response += `• **Total de Contatos**: ${totalContacts.toLocaleString('pt-BR')}\n`; + response += `• **Total de Chats**: ${totalChats.toLocaleString('pt-BR')}\n`; + response += `• **Média de mensagens/chat**: ${(totalMessages / (totalChats || 1)).toFixed(1)}`; + } + + // Resposta padrão + else { + response = `Entendi sua pergunta sobre "${message}". Posso ajudar com:\n\n`; + response += `• **Horários de pico**: "Quais são os horários de maior movimento?"\n`; + response += `• **Análise de sentimento**: "Como está o sentimento geral?"\n`; + response += `• **Estatísticas**: "Quantas mensagens tenho no total?"\n`; + response += `• **Contatos**: "Quais são meus principais contatos?"\n\n`; + response += `💬 Faça uma pergunta mais específica para obter análises detalhadas!`; + } + + return NextResponse.json({ + response, + timestamp: new Date().toISOString(), + }); + } catch (error) { + console.error('Erro no chat:', error); + return NextResponse.json( + { error: 'Erro ao processar mensagem' }, + { status: 500 } + ); + } +} diff --git a/dashboard/app/api/messages/route.ts b/dashboard/app/api/messages/route.ts new file mode 100644 index 00000000..265169a5 --- /dev/null +++ b/dashboard/app/api/messages/route.ts @@ -0,0 +1,62 @@ +import { NextRequest, NextResponse } from 'next/server'; +import prisma from '@/lib/prisma'; + +export async function GET(request: NextRequest) { + try { + const searchParams = request.nextUrl.searchParams; + const instanceId = searchParams.get('instanceId'); + const limit = parseInt(searchParams.get('limit') || '100'); + const offset = parseInt(searchParams.get('offset') || '0'); + const startDate = searchParams.get('startDate'); + const endDate = searchParams.get('endDate'); + const fromMe = searchParams.get('fromMe'); + + // Construir where clause + const where: any = {}; + if (instanceId) { + where.instanceId = instanceId; + } + if (startDate && endDate) { + const startTimestamp = new Date(startDate).getTime(); + const endTimestamp = new Date(endDate).getTime(); + where.messageTimestamp = { + gte: startTimestamp, + lte: endTimestamp, + }; + } + if (fromMe !== null && fromMe !== undefined) { + where.fromMe = fromMe === 'true'; + } + + // Buscar mensagens + const [messages, total] = await Promise.all([ + prisma.message.findMany({ + where, + take: limit, + skip: offset, + orderBy: { messageTimestamp: 'desc' }, + include: { + Instance: { + select: { + name: true, + }, + }, + }, + }), + prisma.message.count({ where }), + ]); + + return NextResponse.json({ + messages, + total, + limit, + offset, + }); + } catch (error) { + console.error('Erro ao buscar mensagens:', error); + return NextResponse.json( + { error: 'Erro ao buscar mensagens' }, + { status: 500 } + ); + } +} diff --git a/dashboard/app/api/sentiment/route.ts b/dashboard/app/api/sentiment/route.ts new file mode 100644 index 00000000..a4e231ff --- /dev/null +++ b/dashboard/app/api/sentiment/route.ts @@ -0,0 +1,98 @@ +import { NextRequest, NextResponse } from 'next/server'; +import prisma from '@/lib/prisma'; +// @ts-ignore +import Sentiment from 'sentiment'; + +const sentiment = new Sentiment(); + +function extractMessageText(messageObj: any): string { + if (typeof messageObj === 'string') return messageObj; + if (!messageObj || typeof messageObj !== 'object') return ''; + + const msg = messageObj.message || messageObj; + + if (msg.conversation) return msg.conversation; + if (msg.extendedTextMessage?.text) return msg.extendedTextMessage.text; + if (msg.imageMessage?.caption) return msg.imageMessage.caption; + if (msg.videoMessage?.caption) return msg.videoMessage.caption; + + return ''; +} + +function categorizeSentiment(score: number): string { + if (score > 2) return 'very_positive'; + if (score > 0) return 'positive'; + if (score < -2) return 'very_negative'; + if (score < 0) return 'negative'; + return 'neutral'; +} + +export async function GET(request: NextRequest) { + try { + const searchParams = request.nextUrl.searchParams; + const instanceId = searchParams.get('instanceId'); + const limit = parseInt(searchParams.get('limit') || '1000'); + + // Construir where clause + const where: any = { + fromMe: false, // Apenas mensagens recebidas + }; + if (instanceId) { + where.instanceId = instanceId; + } + + // Buscar mensagens + const messages = await prisma.message.findMany({ + where, + take: limit, + orderBy: { messageTimestamp: 'desc' }, + }); + + // Analisar sentimento + const analyzed = messages.map((msg: any) => { + const text = extractMessageText(msg.message); + if (!text) return null; + + const analysis = sentiment.analyze(text); + const category = categorizeSentiment(analysis.score); + + return { + id: msg.id, + text, + sentiment: { + score: analysis.score, + comparative: analysis.comparative, + category, + positive: analysis.positive, + negative: analysis.negative, + }, + timestamp: msg.messageTimestamp, + }; + }).filter(Boolean); + + // Calcular distribuição + const distribution = analyzed.reduce((acc: any, item: any) => { + const cat = item.sentiment.category; + acc[cat] = (acc[cat] || 0) + 1; + return acc; + }, {}); + + // Calcular score médio + const avgScore = analyzed.length > 0 + ? analyzed.reduce((sum: number, item: any) => sum + item.sentiment.score, 0) / analyzed.length + : 0; + + return NextResponse.json({ + total: analyzed.length, + avgScore: avgScore.toFixed(2), + distribution, + messages: analyzed.slice(0, 100), // Retornar apenas as primeiras 100 + }); + } catch (error) { + console.error('Erro ao analisar sentimento:', error); + return NextResponse.json( + { error: 'Erro ao analisar sentimento' }, + { status: 500 } + ); + } +} diff --git a/dashboard/app/api/stats/route.ts b/dashboard/app/api/stats/route.ts new file mode 100644 index 00000000..393c9724 --- /dev/null +++ b/dashboard/app/api/stats/route.ts @@ -0,0 +1,92 @@ +import { NextRequest, NextResponse } from 'next/server'; +import prisma from '@/lib/prisma'; + +export async function GET(request: NextRequest) { + try { + const searchParams = request.nextUrl.searchParams; + const instanceId = searchParams.get('instanceId'); + const startDate = searchParams.get('startDate'); + const endDate = searchParams.get('endDate'); + + // Construir where clause + const where: any = {}; + if (instanceId) { + where.instanceId = instanceId; + } + if (startDate && endDate) { + const startTimestamp = new Date(startDate).getTime(); + const endTimestamp = new Date(endDate).getTime(); + where.messageTimestamp = { + gte: startTimestamp, + lte: endTimestamp, + }; + } + + // Buscar estatísticas + const [totalMessages, totalContacts, totalChats, recentMessages] = await Promise.all([ + prisma.message.count({ where }), + prisma.contact.count({ where: instanceId ? { instanceId } : {} }), + prisma.chat.count({ where: instanceId ? { instanceId } : {} }), + prisma.message.findMany({ + where, + take: 100, + orderBy: { messageTimestamp: 'desc' }, + }), + ]); + + // Calcular tempo médio de resposta (simplificado) + let avgResponseTime = '2.5min'; + if (recentMessages.length > 0) { + const conversations = recentMessages.reduce((acc: any, msg: any) => { + const key = JSON.stringify(msg.key); + if (!acc[key]) acc[key] = []; + acc[key].push(msg); + return acc; + }, {}); + + let totalResponseTimes = 0; + let responseCount = 0; + + Object.values(conversations).forEach((msgs: any) => { + for (let i = 1; i < msgs.length; i++) { + if (msgs[i].fromMe !== msgs[i - 1].fromMe) { + const diff = Number(msgs[i].messageTimestamp) - Number(msgs[i - 1].messageTimestamp); + totalResponseTimes += diff; + responseCount++; + } + } + }); + + if (responseCount > 0) { + const avgMs = totalResponseTimes / responseCount; + const avgMinutes = Math.round(avgMs / 60000); + avgResponseTime = `${avgMinutes}min`; + } + } + + // Contar conversas ativas (últimas 24h) + const yesterday = Date.now() - 24 * 60 * 60 * 1000; + const activeConversations = await prisma.chat.count({ + where: { + ...(instanceId ? { instanceId } : {}), + lastMessageTime: { + gte: yesterday, + }, + }, + }); + + return NextResponse.json({ + totalMessages, + totalContacts, + avgResponseTime, + activeConversations, + totalChats, + }); + } catch (error) { + console.error('Erro ao buscar estatísticas:', error); + return NextResponse.json( + { error: 'Erro ao buscar estatísticas' }, + { status: 500 } + ); + } +} diff --git a/dashboard/app/globals.css b/dashboard/app/globals.css new file mode 100644 index 00000000..fdbb5a9a --- /dev/null +++ b/dashboard/app/globals.css @@ -0,0 +1,62 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --foreground-rgb: 0, 0, 0; + --background-start-rgb: 214, 219, 220; + --background-end-rgb: 255, 255, 255; +} + +@media (prefers-color-scheme: dark) { + :root { + --foreground-rgb: 255, 255, 255; + --background-start-rgb: 0, 0, 0; + --background-end-rgb: 0, 0, 0; + } +} + +body { + color: rgb(var(--foreground-rgb)); + background: linear-gradient( + to bottom, + transparent, + rgb(var(--background-end-rgb)) + ) + rgb(var(--background-start-rgb)); +} + +@layer utilities { + .text-balance { + text-wrap: balance; + } +} + +/* Estilos customizados para o chat */ +.chat-message { + @apply p-3 rounded-lg mb-2 max-w-[80%] break-words; +} + +.chat-message.user { + @apply bg-primary-500 text-white ml-auto; +} + +.chat-message.assistant { + @apply bg-gray-200 text-gray-800 mr-auto; +} + +/* Animações suaves */ +.fade-in { + animation: fadeIn 0.3s ease-in; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} diff --git a/dashboard/app/layout.tsx b/dashboard/app/layout.tsx new file mode 100644 index 00000000..36f48c05 --- /dev/null +++ b/dashboard/app/layout.tsx @@ -0,0 +1,22 @@ +import type { Metadata } from "next"; +import { Inter } from "next/font/google"; +import "./globals.css"; + +const inter = Inter({ subsets: ["latin"] }); + +export const metadata: Metadata = { + title: "Evolution Dashboard - Análise de Mensagens WhatsApp", + description: "Painel interativo com IA para análise profunda de mensagens do WhatsApp via Evolution API", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + +
{children} + + ); +} diff --git a/dashboard/app/page.tsx b/dashboard/app/page.tsx new file mode 100644 index 00000000..f4c47274 --- /dev/null +++ b/dashboard/app/page.tsx @@ -0,0 +1,74 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { MessageSquare, Users, TrendingUp, Activity, Send } from 'lucide-react'; +import Dashboard from '@/components/Dashboard'; +import ChatInterface from '@/components/ChatInterface'; +import StatsCards from '@/components/StatsCards'; + +export default function Home() { + const [activeTab, setActiveTab] = useState<'dashboard' | 'chat'>('dashboard'); + + return ( ++ Análise Inteligente de Mensagens WhatsApp +
++ Análise inteligente de dados +
+{message.content}
+ + {message.timestamp.toLocaleTimeString('pt-BR', { + hour: '2-digit', + minute: '2-digit' + })} + ++ Faça perguntas específicas para obter análises mais precisas. Você pode perguntar sobre períodos, contatos, sentimentos e muito mais! +
++ {msg.contact} +
+ + {formatDistanceToNow(msg.time, { addSuffix: true, locale: ptBR })} + ++ {msg.message} +
+ + {getSentimentLabel(msg.sentiment)} + ++ {card.value} +
+| + Contato + | ++ Mensagens + | ++ Tendência + | +
|---|---|---|
|
+
+
+ + {contact.name} + ++ {contact.phone} + + |
+ + + {contact.messages.toLocaleString('pt-BR')} + + | +
+
+ {contact.trend === 'up' ? (
+ <>
+
+ |
+