Files
ComFi/components/AIChatAssistant.tsx
Cauê Faleiros 25384beaa7
Some checks failed
Build and Deploy / build-and-push (push) Failing after 3m18s
chore: setup docker, gitea actions pipeline and docs
2026-02-19 14:25:42 -03:00

269 lines
11 KiB
TypeScript

import React, { useState, useRef, useEffect } from 'react';
import { MessageSquare, X, Send, Bot, User, Sparkles, MinusCircle } from 'lucide-react';
import { GoogleGenAI } from "@google/genai";
interface AIChatAssistantProps {
userName: string;
contextData: {
revenue: number;
expenses: number;
profit: number;
pendingReceivables: number;
}
}
interface Message {
id: string;
role: 'user' | 'assistant';
text: string;
timestamp: Date;
}
export const AIChatAssistant: React.FC<AIChatAssistantProps> = ({ userName, contextData }) => {
if (!process.env.API_KEY) {
return null;
}
const [isOpen, setIsOpen] = useState(false);
const [isTyping, setIsTyping] = useState(false);
const [inputValue, setInputValue] = useState('');
const [messages, setMessages] = useState<Message[]>([
{
id: 'welcome',
role: 'assistant',
text: `Olá ${userName}! 🤖\n\nSou seu consultor especialista no ComFi. Acompanho seus números em tempo real e posso ajudar com:\n\n- **Análise Financeira** (Lucro, Caixa, Despesas)\n- **Estratégias de Crescimento** (Marketing, Vendas)\n- **Gestão Operacional**\n\nComo posso ajudar seu negócio hoje?`,
timestamp: new Date()
}
]);
const messagesEndRef = useRef<HTMLDivElement>(null);
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
};
useEffect(() => {
scrollToBottom();
}, [messages, isOpen]);
// Função para processar formatação de texto (Negrito e Listas)
const renderFormattedText = (text: string) => {
return text.split('\n').map((line, index) => {
// Processar item de lista
if (line.trim().startsWith('- ')) {
const content = line.trim().substring(2);
return (
<div key={index} className="flex items-start gap-2 mb-1 pl-1">
<span className="min-w-[6px] h-[6px] rounded-full bg-current mt-1.5 opacity-60"></span>
<span className="flex-1">{parseBold(content)}</span>
</div>
);
}
// Processar linha vazia
if (line.trim() === '') {
return <div key={index} className="h-2"></div>;
}
// Parágrafo normal
return <p key={index} className="mb-1 last:mb-0 leading-relaxed">{parseBold(line)}</p>;
});
};
const parseBold = (text: string) => {
return text.split(/(\*\*.*?\*\*)/).map((part, i) => {
if (part.startsWith('**') && part.endsWith('**')) {
return <strong key={i} className="font-bold">{part.slice(2, -2)}</strong>;
}
return part;
});
};
const handleSend = async () => {
if (!inputValue.trim()) return;
const userMsg: Message = {
id: Math.random().toString(),
role: 'user',
text: inputValue,
timestamp: new Date()
};
setMessages(prev => [...prev, userMsg]);
setInputValue('');
setIsTyping(true);
try {
// Inicializar Cliente Gemini
const ai = new GoogleGenAI({ apiKey: process.env.API_KEY });
// Construir Instrução do Sistema com Contexto Financeiro Atual
const systemInstruction = `
Você é o **ComFi Assistant**, um consultor de elite em gestão empresarial e marketing, integrado ao sistema ComFi.
**CONTEXTO FINANCEIRO ATUAL DO USUÁRIO:**
- Receita: R$ ${contextData.revenue.toLocaleString('pt-BR')}
- Despesas: R$ ${contextData.expenses.toLocaleString('pt-BR')}
- Lucro Líquido: R$ ${contextData.profit.toLocaleString('pt-BR')}
- A Receber (Pendente): R$ ${contextData.pendingReceivables.toLocaleString('pt-BR')}
**SUA MISSÃO:**
Atuar como um estrategista sênior. Analise os dados e a pergunta do usuário para fornecer conselhos práticos, ideias de marketing criativas e insights financeiros.
**DIRETRIZES DE RESPOSTA (RIGOROSO):**
1. **Formatação Limpa:** JAMAIS escreva blocos de texto longos. Use parágrafos curtos.
2. **Uso de Listas:** Sempre que apresentar passos, ideias ou dados, use listas com marcadores (\`- \`).
3. **Destaques:** Use **negrito** para números importantes e termos-chave.
4. **Tom de Voz:** Profissional, especialista, motivador e direto ao ponto.
5. **Foco em Ação:** Dê sugestões que o usuário possa implementar hoje.
Exemplo de formato ideal:
"Baseado nos seus dados, aqui estão 3 ações:
- **Ação 1**: Explicação breve.
- **Ação 2**: Explicação breve."
`;
// Chamada à API
const response = await ai.models.generateContent({
model: 'gemini-3-flash-preview',
contents: [
{ role: 'user', parts: [{ text: inputValue }] }
],
config: {
systemInstruction: systemInstruction,
temperature: 0.7, // Criatividade balanceada para marketing
}
});
const text = response.text || "Desculpe, não consegui gerar uma resposta no momento.";
const botMsg: Message = {
id: Math.random().toString(),
role: 'assistant',
text: text,
timestamp: new Date()
};
setMessages(prev => [...prev, botMsg]);
} catch (error) {
console.error("Erro ao chamar Gemini API:", error);
const errorMsg: Message = {
id: Math.random().toString(),
role: 'assistant',
text: "Desculpe, estou enfrentando uma instabilidade temporária na minha conexão neural. 🧠\n\nPor favor, tente novamente em alguns instantes.",
timestamp: new Date()
};
setMessages(prev => [...prev, errorMsg]);
} finally {
setIsTyping(false);
}
};
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') handleSend();
};
return (
<>
{/* Floating Button */}
<button
onClick={() => setIsOpen(!isOpen)}
className={`fixed bottom-6 right-6 z-50 p-4 rounded-full shadow-2xl transition-all duration-300 hover:scale-110 flex items-center justify-center ${
isOpen ? 'bg-slate-800 rotate-90' : 'bg-gradient-to-r from-primary-500 to-orange-600'
}`}
>
{isOpen ? <X color="white" size={24} /> : <MessageSquare color="white" size={28} fill="currentColor" className="text-white/20" />}
{!isOpen && (
<span className="absolute top-0 right-0 w-3 h-3 bg-red-500 border-2 border-white rounded-full"></span>
)}
</button>
{/* Chat Window */}
<div
className={`fixed bottom-24 right-6 w-96 max-w-[calc(100vw-3rem)] bg-white rounded-2xl shadow-2xl border border-slate-100 z-50 flex flex-col transition-all duration-300 origin-bottom-right overflow-hidden ${
isOpen ? 'opacity-100 scale-100 translate-y-0' : 'opacity-0 scale-90 translate-y-10 pointer-events-none'
}`}
style={{ height: '550px' }}
>
{/* Header */}
<div className="bg-slate-900 p-4 flex items-center gap-3 shadow-md relative z-10">
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-primary-400 to-orange-600 flex items-center justify-center border-2 border-slate-700 shadow-inner">
<Bot size={20} className="text-white" />
</div>
<div className="flex-1">
<h3 className="text-white font-bold text-sm flex items-center gap-2">
ComFi Especialista <span className="bg-primary-500 text-[10px] px-1.5 py-0.5 rounded text-white font-bold tracking-wide">AI</span>
</h3>
<p className="text-slate-400 text-xs flex items-center gap-1">
<span className="w-2 h-2 rounded-full bg-green-500 animate-pulse"></span> Consultor Online
</p>
</div>
<button onClick={() => setIsOpen(false)} className="text-slate-400 hover:text-white transition-colors"><MinusCircle size={18}/></button>
</div>
{/* Messages Area */}
<div className="flex-1 overflow-y-auto p-4 space-y-4 bg-[#F8FAFC] scrollbar-thin">
{messages.map((msg) => (
<div key={msg.id} className={`flex gap-3 ${msg.role === 'user' ? 'flex-row-reverse' : ''} animate-fade-in`}>
<div className={`w-8 h-8 rounded-full flex items-center justify-center shrink-0 shadow-sm border border-black/5 ${
msg.role === 'user' ? 'bg-white text-slate-600' : 'bg-primary-100 text-primary-600'
}`}>
{msg.role === 'user' ? <User size={14} /> : <Sparkles size={14} />}
</div>
<div className={`max-w-[85%] p-3.5 rounded-2xl text-sm shadow-sm ${
msg.role === 'user'
? 'bg-white text-slate-700 rounded-tr-none border border-slate-100'
: 'bg-white text-slate-800 rounded-tl-none border border-slate-100 shadow-md'
}`}>
{msg.role === 'assistant' ? (
<div className="text-slate-600">
{renderFormattedText(msg.text)}
</div>
) : (
msg.text
)}
<div className={`text-[10px] mt-2 text-right opacity-50 font-medium`}>
{msg.timestamp.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}
</div>
</div>
</div>
))}
{isTyping && (
<div className="flex gap-3 animate-pulse">
<div className="w-8 h-8 rounded-full bg-primary-100 text-primary-600 flex items-center justify-center">
<Sparkles size={14} />
</div>
<div className="bg-white border border-slate-100 p-4 rounded-2xl rounded-tl-none flex gap-1 items-center shadow-sm">
<span className="w-1.5 h-1.5 bg-slate-400 rounded-full animate-bounce"></span>
<span className="w-1.5 h-1.5 bg-slate-400 rounded-full animate-bounce" style={{animationDelay: '0.1s'}}></span>
<span className="w-1.5 h-1.5 bg-slate-400 rounded-full animate-bounce" style={{animationDelay: '0.2s'}}></span>
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
{/* Input Area */}
<div className="p-3 bg-white border-t border-slate-100 flex gap-2 shadow-[0_-4px_6px_-1px_rgba(0,0,0,0.05)] relative z-20">
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyPress={handleKeyPress}
placeholder="Pergunte sobre lucro, ideias de venda..."
className="flex-1 bg-slate-50 border border-slate-200 rounded-xl px-4 py-3 text-sm outline-none focus:ring-2 focus:ring-primary-200 text-slate-700 placeholder-slate-400 transition-all"
/>
<button
onClick={handleSend}
disabled={!inputValue.trim()}
className="w-11 h-11 bg-primary-500 text-white rounded-xl flex items-center justify-center hover:bg-primary-600 disabled:opacity-50 disabled:hover:bg-primary-500 transition-all shadow-lg shadow-primary-200 active:scale-95"
>
<Send size={18} />
</button>
</div>
</div>
</>
);
};