269 lines
11 KiB
TypeScript
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>
|
|
</>
|
|
);
|
|
}; |