feat: Initialize ComFi project with Vite

Setup project structure, dependencies, and basic configuration for the ComFi application. Includes initial setup for Vite, React, TypeScript, Tailwind CSS, and essential development tools. Defines core types and provides a basic README for local development.
This commit is contained in:
MMrp89
2026-02-09 20:28:37 -03:00
parent 1e6a56d866
commit 1a57ac7754
28 changed files with 6070 additions and 8 deletions

View File

@@ -0,0 +1,265 @@
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 }) => {
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>
</>
);
};