Compreenda a estrutura completa da solução de agentes de IA com Gemini
Context API, Hooks customizados e Componentes de UI para interação com os agentes
API REST, GeminiService, Sistema de Ferramentas e validação de API Keys
Modelo gemini-2.0-flash-exp com suporte a Function Calling e conversação contextual
┌─────────────────────┐
│ FRONTEND (React) │
│ - Context/Hooks │
│ - Components │
└──────────┬──────────┘
│ HTTP/REST
┌──────────▼──────────┐
│ BACKEND (Node.js) │
│ - Routes │
│ - GeminiService │
│ - AgentTools │
└──────────┬──────────┘
│ API Call
┌──────────▼──────────┐
│ Google Gemini API │
│ - AI Processing │
│ - Function Calling │
└─────────────────────┘
Criptografia de API Keys e validação em múltiplas camadas
Cache inteligente e optimistic updates para UX fluida
Sistema de ferramentas modulares via Function Calling
Agentes especializados com contexto e memória persistente
Dependências, variáveis de ambiente e configurações essenciais
{
"dependencies": {
"@google/generative-ai": "^0.2.1",
"express": "^4.18.2",
"cors": "^2.8.5",
"dotenv": "^16.0.3",
"crypto": "^1.0.1"
}
}
# API Key do Gemini (Sistema) GEMINI_API_KEY=AIzaSy... # Configurações da API PORT=3001 NODE_ENV=development # Segurança ENCRYPTION_KEY=sua-chave-32-caracteres
{
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"axios": "^1.4.0"
}
}
# URL da API Backend VITE_API_URL=http://localhost:3001/api/v1 # Ambiente VITE_NODE_ENV=development
Crie o arquivo backend/src/config/gemini.config.ts com as seguintes configurações:
export const geminiConfig = {
// API Key global do sistema
systemApiKey: process.env.GEMINI_API_KEY || null,
// Configurações de segurança
security: {
encryptUserKeys: true,
hashAlgorithm: 'sha256',
keyMasking: true,
validationCacheTTL: 3600 // 1 hora
},
// Configurações padrão do modelo
defaults: {
model: 'gemini-2.0-flash-exp',
temperature: 0.7,
topP: 0.95,
topK: 40,
maxOutputTokens: 2048
},
// Modelos disponíveis
availableModels: [
{
id: 'gemini-2.0-flash-exp',
name: 'Gemini 2.0 Flash (Experimental)',
description: 'Modelo rápido e eficiente',
features: ['function-calling', 'chat', 'streaming']
},
{
id: 'gemini-1.5-pro',
name: 'Gemini 1.5 Pro',
description: 'Modelo avançado para tarefas complexas',
features: ['function-calling', 'chat', 'long-context']
}
]
};
Function Calling - Permitindo que agentes executem ações em tempo real
Function Calling é uma funcionalidade do Gemini que permite ao modelo de IA identificar quando precisa de dados externos para responder adequadamente a uma pergunta. Em vez de apenas gerar texto, o modelo pode solicitar a execução de funções específicas.
Usuário: "Quantos veículos disponíveis temos?"
Gemini detecta:
{
functionCall: {
name: "listar_veiculos",
args: { status: "disponivel" }
}
}
Sistema executa:
const veiculos = await buscarVeiculos({
status: "disponivel"
});
Gemini gera resposta:
"Atualmente temos 2 veículos disponíveis:
- ABC-1234
- XYZ-9876"
As ferramentas são organizadas por tipo de agente. Cada ferramenta possui nome, descrição e parâmetros.
export interface ToolDefinition {
name: string;
description: string;
parameters: {
type: "object";
properties: Record<string, {
type: string;
description: string;
enum?: string[];
}>;
required: string[];
};
}
// Ferramentas do Agente de Logística (ALP)
export const AGENT_TOOLS: Record<string, ToolDefinition[]> = {
ALP: [
{
name: "listar_veiculos",
description: "Lista veículos com filtros opcionais",
parameters: {
type: "object",
properties: {
status: {
type: "string",
description: "Status do veículo",
enum: ["disponivel", "em_rota", "manutencao"]
},
tipo: {
type: "string",
description: "Tipo do veículo"
}
},
required: []
}
},
{
name: "criar_rota",
description: "Cria nova rota de entrega",
parameters: {
type: "object",
properties: {
origem: {
type: "string",
description: "Endereço de origem"
},
destino: {
type: "string",
description: "Endereço de destino"
},
veiculo_id: {
type: "string",
description: "ID do veículo"
}
},
required: ["origem", "destino"]
}
}
]
};
A classe AgentToolExecutor é responsável por executar as ferramentas de forma segura e controlada.
export class AgentToolExecutor {
static async executeTool(
agentId: string,
toolName: string,
args: Record<string, any>
): Promise<any> {
// 1. Validar agente
const agent = await findAgentById(agentId);
if (!agent) {
throw new Error('Agente não encontrado');
}
// 2. Verificar permissões
const tools = AGENT_TOOLS[agent.type];
const tool = tools?.find(t => t.name === toolName);
if (!tool) {
throw new Error(
`Ferramenta ${toolName} não disponível`
);
}
// 3. Executar ação
switch (toolName) {
case "listar_veiculos":
return await this.listarVeiculos(args);
case "criar_rota":
return await this.criarRota(args);
default:
throw new Error('Ferramenta não implementada');
}
}
private static async listarVeiculos(
args: { status?: string; tipo?: string }
) {
// Busca no banco de dados
const veiculos = await database.veiculos.findMany({
where: {
...(args.status && { status: args.status }),
...(args.tipo && { tipo: args.tipo })
}
});
return {
total: veiculos.length,
veiculos: veiculos.map(v => ({
id: v.id,
placa: v.placa,
status: v.status,
tipo: v.tipo
}))
};
}
}
Verifica se o agente existe e tem permissão para usar a ferramenta
Checa se os argumentos estão de acordo com a definição
Cada ferramenta roda em contexto controlado e seguro
Endpoints REST, estrutura de requisições e respostas
http://localhost:3001/api/v1
/agents - Gerenciamento de agentes/conversations - Conversas e histórico/messages - Envio de mensagens/tools - Execução de ferramentas{
"Content-Type": "application/json",
"Accept": "application/json",
"X-API-Version": "1.0",
"X-Client-Id": "react-app"
}
/agents
Lista todos os agentes disponíveis
{
"success": true,
"data": [
{
"id": "agent-1",
"name": "Agente de Logística",
"type": "ALP",
"modelName": "gemini-2.0-flash-exp",
"status": "active"
}
]
}
/conversations
Inicia uma nova conversa com um agente
{
"agentId": "agent-1",
"title": "Nova conversa",
"metadata": {
"source": "web-app"
}
}
{
"success": true,
"data": {
"id": "conv-123",
"agentId": "agent-1",
"title": "Nova conversa",
"messages": [],
"createdAt": "2025-10-14T10:00:00Z"
}
}
/conversations/:id/messages
Envia mensagem para um agente
{
"content": "Quantos veículos disponíveis?",
"role": "user",
"userApiKey": "AIzaSy..." // opcional
}
{
"success": true,
"data": {
"id": "msg-456",
"conversationId": "conv-123",
"role": "assistant",
"content": "Temos 2 veículos disponíveis...",
"confidence": 0.95,
"toolCalls": [],
"timestamp": "2025-10-14T10:01:00Z"
}
}
/tools/execute
Executa uma ferramenta específica
{
"agentId": "agent-1",
"toolName": "listar_veiculos",
"args": {
"status": "disponivel"
}
}
{
"success": true,
"data": {
"total": 2,
"veiculos": [
{ "id": "v1", "placa": "ABC-1234" },
{ "id": "v2", "placa": "XYZ-9876" }
]
}
}
Dados de entrada inválidos
{
"success": false,
"error": {
"code": "INVALID_INPUT",
"message": "Campo obrigatório"
}
}
API Key inválida ou ausente
{
"success": false,
"error": {
"code": "INVALID_API_KEY",
"message": "API Key inválida"
}
}
Erro interno do servidor
{
"success": false,
"error": {
"code": "INTERNAL_ERROR",
"message": "Erro inesperado"
}
}
{ success: boolean, data?: any, error?: Error }
para facilitar o tratamento no frontend.
Entenda o ciclo completo desde a mensagem do usuário até a resposta da IA
┌─────────────────────────────────────────────────────────┐
│ 1. FRONTEND - Usuário Envia Mensagem │
└────────────────────────┬────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ 2. CONTEXT - Optimistic Update │
│ • Adiciona mensagem do usuário imediatamente │
│ • UI atualiza instantaneamente │
└────────────────────────┬────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ 3. API CALL - POST /conversations/:id/messages │
│ • Envia payload com mensagem e histórico │
└────────────────────────┬────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ 4. BACKEND - GeminiService.sendMessage() │
│ • Valida API Key │
│ • Gera prompt do sistema │
│ • Constrói histórico da conversa │
└────────────────────────┬────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ 5. GEMINI API - Processamento de IA │
│ • Analisa mensagem e contexto │
│ • Decide se precisa executar ferramentas │
└────────────────────────┬────────────────────────────────┘
│
┌────┴────┐
│ │
┌─────▼─────┐ ▼ (Sem ferramentas)
│ 6. TOOLS │
│ EXECUTION │
└─────┬─────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ 7. GEMINI - Gera Resposta Final │
│ • Incorpora resultados das ferramentas │
│ • Formata resposta natural │
└────────────────────────┬────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ 8. BACKEND - Processa Resposta │
│ • Calcula confiança │
│ • Salva mensagem no banco │
│ • Retorna JSON para frontend │
└────────────────────────┬────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ 9. FRONTEND - Atualiza UI │
│ • Substitui mensagem temporária │
│ • Exibe resposta do agente │
└─────────────────────────────────────────────────────────┘
"Quantos veículos disponíveis temos?"
setMessages(prev => [...prev, {
id: 'temp-123',
role: 'user',
content: 'Quantos veículos...',
timestamp: new Date()
}]);
POST /api/v1/conversations/conv-123/messages
{
"content": "Quantos veículos...",
"role": "user"
}
const response = await geminiService.sendMessage( agentConfig, message.content, conversationHistory, userApiKey );
// Gemini detecta necessidade de ferramenta
{
functionCall: {
name: "listar_veiculos",
args: { status: "disponivel" }
}
}
const result = await AgentToolExecutor.executeTool(
agentId,
'listar_veiculos',
{ status: 'disponivel' }
);
// Retorna:
{
total: 2,
veiculos: [
{ id: 'v1', placa: 'ABC-1234' },
{ id: 'v2', placa: 'XYZ-9876' }
]
}
// Com os dados da ferramenta, Gemini gera: "Atualmente temos 2 veículos disponíveis: - ABC-1234 - XYZ-9876"
{
"success": true,
"data": {
"id": "msg-456",
"role": "assistant",
"content": "Atualmente temos 2...",
"confidence": 0.95,
"toolCalls": [{
"name": "listar_veiculos",
"result": { ... }
}],
"timestamp": "2025-10-14T10:01:30Z"
}
}
// Remove temporária, adiciona real
setMessages(prev => [
...prev.filter(m => !m.id.startsWith('temp-')),
aiMessage
]);
Remove mensagem optimistic, exibe erro ao usuário e mantém histórico anterior intacto
Gemini pode solicitar execução de várias ferramentas em sequência para responder adequadamente
Se resposta demorar mais de 30s, retorna erro e permite que usuário tente novamente
Otimizações, cache e estratégias para uma experiência fluida
Valida API Keys apenas uma vez por hora, armazenando resultado em memória
interface ApiKeyValidationCache {
[keyHash: string]: {
isValid: boolean;
timestamp: number;
expiresAt: number;
};
}
private validationCache: ApiKeyValidationCache = {};
async validateApiKeyWithCache(apiKey: string) {
const keyHash = ApiKeySecurity.hash(apiKey);
const cached = this.validationCache[keyHash];
// Verifica se cache ainda é válido
if (cached && Date.now() < cached.expiresAt) {
return cached.isValid;
}
// Valida novamente
const isValid = await this.testConnection(apiKey);
// Atualiza cache (TTL: 1 hora)
this.validationCache[keyHash] = {
isValid,
timestamp: Date.now(),
expiresAt: Date.now() + 3600000
};
return isValid;
}
Armazena conversas ativas no Context para evitar requisições desnecessárias
// No AIChatContext
const [conversationsCache, setConversationsCache] =
useState<Map<string, Conversation>>(new Map());
const getConversation = useCallback(
async (id: string) => {
// Verifica cache primeiro
if (conversationsCache.has(id)) {
return conversationsCache.get(id);
}
// Busca do servidor
const response = await api.get(`/conversations/${id}`);
const conversation = response.data;
// Atualiza cache
setConversationsCache(prev =>
new Map(prev).set(id, conversation)
);
return conversation;
},
[conversationsCache]
);
Atualiza a UI imediatamente assumindo que a operação será bem-sucedida, sem esperar resposta do servidor. Se falhar, reverte a mudança.
const sendMessage = async (content: string) => {
if (!activeConversation) return;
// 1. Mensagem temporária
const tempId = `temp-${Date.now()}`;
const userMessage = {
id: tempId,
role: 'user',
content,
timestamp: new Date(),
sending: true // flag visual
};
// 2. Atualiza UI imediatamente
setActiveConv(prev => ({
...prev!,
messages: [...prev!.messages, userMessage]
}));
try {
// 3. Envia para servidor
const response = await api.post(
`/conversations/${activeConversation.id}/messages`,
{ content, role: 'user' }
);
// 4. Substitui mensagem temporária
setActiveConv(prev => ({
...prev!,
messages: prev!.messages.map(m =>
m.id === tempId
? { ...response.data, sending: false }
: m
)
}));
// 5. Adiciona resposta do agente
const aiMessage = response.data.aiResponse;
setActiveConv(prev => ({
...prev!,
messages: [...prev!.messages, aiMessage]
}));
} catch (error) {
// 6. Rollback em caso de erro
setActiveConv(prev => ({
...prev!,
messages: prev!.messages.filter(
m => m.id !== tempId
)
}));
// 7. Notifica usuário
showError('Falha ao enviar mensagem');
}
};
Evita re-renderizações desnecessárias de componentes
const ChatMessage = React.memo(
({ message }) => (
<div className="message">
{message.content}
</div>
),
(prev, next) =>
prev.message.id === next.message.id
);
Memoiza funções para evitar recriação
const handleSend = useCallback(
(message: string) => {
sendChatMessage(message);
},
[sendChatMessage]
);
Renderiza apenas mensagens visíveis
import { FixedSizeList } from
'react-window';
<FixedSizeList
height={600}
itemCount={messages.length}
itemSize={80}
>
{MessageRow}
</FixedSizeList>
| Métrica | Alvo | Medição | Otimização |
|---|---|---|---|
| Time to First Message | < 100ms | Optimistic update | ✅ Implementado |
| API Response Time | < 2s | Gemini + Backend | ✅ Cache de API Keys |
| UI Update Time | < 50ms | React re-render | ✅ React.memo + Virtual List |
| Conversation Load | < 300ms | Navegação entre conversas | ✅ Cache de conversas |
| Memory Usage | < 100MB | Limite de mensagens em cache | ⚠️ Limpar conversas antigas |
Proteja sua aplicação e dados dos usuários
Chaves no código cliente podem ser extraídas e abusadas
Backend valida, protege e intermedia todas as chamadas
Se armazenar keys de usuários, sempre criptografe
// ApiKeySecurity.ts
import crypto from 'crypto';
export class ApiKeySecurity {
private static ALGORITHM = 'aes-256-gcm';
private static KEY = process.env.ENCRYPTION_KEY!;
// Criptografa API Key
static encrypt(apiKey: string): string {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(
this.ALGORITHM,
Buffer.from(this.KEY, 'hex'),
iv
);
let encrypted = cipher.update(apiKey, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
return `${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted}`;
}
// Descriptografa API Key
static decrypt(encryptedKey: string): string {
const [ivHex, authTagHex, encrypted] = encryptedKey.split(':');
const decipher = crypto.createDecipheriv(
this.ALGORITHM,
Buffer.from(this.KEY, 'hex'),
Buffer.from(ivHex, 'hex')
);
decipher.setAuthTag(Buffer.from(authTagHex, 'hex'));
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
// Hash para cache (one-way)
static hash(apiKey: string): string {
return crypto
.createHash('sha256')
.update(apiKey)
.digest('hex');
}
// Valida formato de API Key
static validateFormat(apiKey: string): boolean {
return /^AIza[0-9A-Za-z\-_]{35}$/.test(apiKey);
}
// Mascara key para logs
static mask(apiKey: string): string {
if (apiKey.length < 8) return '***';
return `${apiKey.substring(0, 4)}...${apiKey.substring(apiKey.length - 4)}`;
}
}
// Middleware de validação
const validateMessageInput = (req, res, next) => {
const { content, role } = req.body;
// Valida presença
if (!content || typeof content !== 'string') {
return res.status(400).json({
success: false,
error: {
code: 'INVALID_CONTENT',
message: 'Content é obrigatório'
}
});
}
// Limita tamanho
if (content.length > 10000) {
return res.status(400).json({
success: false,
error: {
code: 'CONTENT_TOO_LONG',
message: 'Máximo 10.000 caracteres'
}
});
}
// Sanitiza HTML/Scripts
req.body.content = sanitizeHtml(content, {
allowedTags: [],
allowedAttributes: {}
});
// Valida role
if (!['user', 'assistant'].includes(role)) {
return res.status(400).json({
success: false,
error: {
code: 'INVALID_ROLE',
message: 'Role deve ser user ou assistant'
}
});
}
next();
};
import rateLimit from 'express-rate-limit';
// Limita requisições por IP
const messageLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minuto
max: 20, // 20 mensagens por minuto
message: {
success: false,
error: {
code: 'RATE_LIMIT_EXCEEDED',
message: 'Muitas requisições. Aguarde 1 minuto.'
}
},
standardHeaders: true,
legacyHeaders: false
});
// Aplica no endpoint
app.post(
'/api/v1/conversations/:id/messages',
messageLimiter,
validateMessageInput,
sendMessageHandler
);
import winston from 'winston';
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.File({
filename: 'error.log',
level: 'error'
}),
new winston.transports.File({
filename: 'combined.log'
})
]
});
// ✅ Correto - API Key mascarada
logger.info('API Key validada', {
keyHash: ApiKeySecurity.hash(apiKey),
keyMask: ApiKeySecurity.mask(apiKey),
userId: user.id
});
// ❌ ERRADO - Expõe API Key
logger.info('API Key validada', {
apiKey: apiKey // NUNCA FAÇA ISSO!
});
Casos de uso reais e implementações completas
{
"id": "agent-suporte",
"name": "Agente de Suporte",
"type": "CUSTOMER_SUPPORT",
"modelName": "gemini-2.0-flash-exp",
"systemPrompt": `
Você é um assistente de atendimento ao cliente.
Regras:
- Seja educado e empático
- Pergunte informações necessárias
- Use ferramentas para consultar dados
- Resolva problemas de forma proativa
- Escale para humano quando necessário
`,
"temperature": 0.7,
"tools": [
"buscar_pedido",
"consultar_estoque",
"abrir_ticket",
"verificar_garantia"
]
}
export const SUPPORT_TOOLS = [
{
name: "buscar_pedido",
description: "Busca informações de um pedido",
parameters: {
type: "object",
properties: {
pedido_id: {
type: "string",
description: "ID do pedido"
}
},
required: ["pedido_id"]
}
},
{
name: "abrir_ticket",
description: "Abre ticket de suporte",
parameters: {
type: "object",
properties: {
titulo: { type: "string" },
descricao: { type: "string" },
prioridade: {
type: "string",
enum: ["baixa", "media", "alta"]
}
},
required: ["titulo", "descricao"]
}
}
];
Olá, meu pedido #12345 não chegou ainda
[Executa: buscar_pedido({ pedido_id: "12345" })]
Olá! Verifiquei seu pedido #12345. Ele foi despachado ontem e está em trânsito. Previsão de entrega: 14/10/2025. Você pode acompanhar em tempo real pelo código de rastreio: BR123456789.
Mas eu preciso urgente. Dá para adiantar?
[Executa: abrir_ticket({ titulo: "Entrega urgente", prioridade: "alta" })]
Entendo a urgência! Abri um ticket prioritário (#T-789) para nossa equipe logística verificar possibilidades de entrega expressa. Você receberá retorno em até 2 horas. Posso ajudar com mais alguma coisa?
{
"id": "agent-analytics",
"name": "Analista de Dados IA",
"type": "DATA_ANALYST",
"modelName": "gemini-1.5-pro",
"systemPrompt": `
Você é um analista de dados especializado.
Capacidades:
- Interpretar queries SQL
- Gerar visualizações
- Identificar tendências
- Fazer previsões baseadas em dados
- Explicar insights de forma clara
`,
"temperature": 0.3,
"tools": [
"executar_query",
"gerar_grafico",
"calcular_metricas"
]
}
async executarQuery(args: { query: string }) {
// Valida query (previne SQL injection)
const sanitizedQuery = this.sanitizeSQL(args.query);
// Limita queries a SELECT apenas
if (!sanitizedQuery.toLowerCase().startsWith('select')) {
throw new Error('Apenas queries SELECT permitidas');
}
// Executa com timeout
const result = await database.query(
sanitizedQuery,
{ timeout: 5000 }
);
return {
rows: result.rows.slice(0, 100), // Limita resultados
totalCount: result.rowCount,
executionTime: result.duration
};
}
Quais foram as vendas totais do último trimestre?
[Executa: executar_query({ query: "SELECT SUM(valor) as total FROM vendas WHERE data >= '2024-07-01' AND data <= '2024-09-30'" })]
Análise do Q3 2024 (Jul-Set):
Comparado ao Q2, houve crescimento de 15%. Posso gerar um gráfico?
Sim, por favor! E me mostre os top 5 produtos
[Executa: gerar_grafico({ tipo: "linha", dados: [...] })]
[Executa: executar_query({ query: "SELECT produto, SUM(valor) as total FROM vendas GROUP BY produto ORDER BY total DESC LIMIT 5" })]
📊 Gráfico gerado! Top 5 produtos:
import React, { useState, useEffect } from 'react';
import { useAIChat } from '../contexts/AIChatContext';
export const ChatPanel: React.FC = () => {
const {
activeConversation,
sendChatMessage,
loading
} = useAIChat();
const [message, setMessage] = useState('');
const [messages, setMessages] = useState<ChatMessage[]>([]);
// Atualiza mensagens quando conversa muda
useEffect(() => {
if (activeConversation) {
setMessages(activeConversation.messages);
}
}, [activeConversation]);
const handleSend = async (e: React.FormEvent) => {
e.preventDefault();
if (!message.trim() || loading) return;
const currentMessage = message;
setMessage(''); // Limpa input imediatamente
try {
await sendChatMessage(currentMessage);
} catch (error) {
console.error('Erro ao enviar mensagem:', error);
setMessage(currentMessage); // Restaura em caso de erro
}
};
return (
<div className="chat-panel">
{/* Header */}
<div className="chat-header">
<h2>{activeConversation?.agent.name || 'Selecione um agente'}</h2>
{activeConversation && (
<span className="badge">
{activeConversation.agent.type}
</span>
)}
</div>
{/* Mensagens */}
<div className="messages-container">
{messages.map((msg) => (
<div
key={msg.id}
className={`message ${msg.role}`}
>
<div className="message-avatar">
{msg.role === 'user' ? '👤' : '🤖'}
</div>
<div className="message-content">
<div className="message-text">{msg.content}</div>
<div className="message-meta">
<span>{formatTime(msg.timestamp)}</span>
{msg.confidence && (
<span>Confiança: {(msg.confidence * 100).toFixed(0)}%</span>
)}
{msg.sending && <span>Enviando...</span>}
</div>
</div>
</div>
))}
{loading && (
<div className="typing-indicator">
<span></span><span></span><span></span>
</div>
)}
</div>
{/* Input */}
<form onSubmit={handleSend} className="chat-input">
<input
type="text"
value={message}
onChange={(e) => setMessage(e.target.value)}
placeholder="Digite sua mensagem..."
disabled={!activeConversation || loading}
/>
<button
type="submit"
disabled={!message.trim() || loading}
>
Enviar
</button>
</form>
</div>
);
};
// Utilitário de formatação
function formatTime(date: Date): string {
return new Intl.DateTimeFormat('pt-BR', {
hour: '2-digit',
minute: '2-digit'
}).format(new Date(date));
}
Para começar, por favor nos informe: