klawdyoss's picture
salvando
8a0e39e
import React, { useState, useRef, useEffect } from 'react';
import { MdSend, MdRefresh, MdConstruction } from 'react-icons/md';
import { FiUser, FiCpu, FiAlertTriangle, FiInfo } from 'react-icons/fi';
import { v4 as uuidv4 } from 'uuid';
import { toast } from "react-toastify";
import classNames from "classnames";
import ChatMessageComponent, { ChatMessage, MessageType } from './ChatMessage';
import { defaultHTML } from "../../utils/consts";
interface ChatInterfaceProps {
agentId: string;
html: string;
setHtml: (h: string) => void;
isAiWorking: boolean;
setisAiWorking: (b: boolean) => void;
onNewPrompt: (p: string) => void;
onScrollToBottom: () => void;
geminiKey: string | undefined;
chatgptKey: string | undefined;
hfToken: string | undefined;
}
const ChatInterface: React.FC<ChatInterfaceProps> = ({
agentId,
html,
setHtml,
isAiWorking,
setisAiWorking,
onNewPrompt,
onScrollToBottom,
geminiKey,
chatgptKey,
hfToken
}) => {
const [prompt, setPrompt] = useState("");
const [messages, setMessages] = useState<ChatMessage[]>([]);
const chatEndRef = useRef<HTMLDivElement>(null);
const [latestHtml, setLatestHtml] = useState<string>(html);
const [isTyping, setIsTyping] = useState(false);
// Efeito para rolar para o final quando novas mensagens chegarem
useEffect(() => {
chatEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
// Função principal para enviar mensagem
const send = async () => {
if (!prompt.trim() || isAiWorking) return;
setisAiWorking(true);
onNewPrompt(prompt);
// Adiciona mensagem do usuário ao histórico
const userMessage: ChatMessage = {
id: uuidv4(),
type: 'user',
content: prompt,
timestamp: new Date()
};
setMessages(prev => [...prev, userMessage]);
try {
// Registra o contexto atual para o agente orquestrador (opcional)
const context = {
currentHtml: html !== defaultHTML ? html : "Sem HTML ainda",
previousMessages: messages.slice(-5), // Últimas 5 mensagens
agentId
};
const resp = await fetch(`/api/agent/${agentId}/message`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
prompt,
html: html === defaultHTML ? undefined : html,
gemini_api_key: geminiKey,
chatgpt_api_key: chatgptKey,
hf_token: hfToken,
provider: "auto",
context: JSON.stringify(context) // Enviando contexto para o servidor
}),
});
const reader = resp.body?.getReader();
const dec = new TextDecoder("utf-8");
let partial = "";
let agentReply: ChatMessage = {
id: uuidv4(),
type: 'agent',
content: '',
agentId,
timestamp: new Date()
};
// Adiciona a mensagem vazia inicial do agente
setMessages(prev => [...prev, agentReply]);
while (true) {
const { done, value } = await reader!.read();
if (done) break;
const chunk = dec.decode(value);
// Processa os logs separadamente
const lines = chunk.split('\n');
for (const line of lines) {
if (line.trim().startsWith('[LOG]')) {
// Adiciona logs como mensagens de tipo log
const logMessage: ChatMessage = {
id: uuidv4(),
type: 'log',
content: line.replace('[LOG] ', ''),
timestamp: new Date()
};
setMessages(prev => [...prev, logMessage]);
} else if (line.trim().startsWith('❌')) {
// Adiciona erros como mensagens de tipo error
const errorMessage: ChatMessage = {
id: uuidv4(),
type: 'error',
content: line,
timestamp: new Date()
};
// Se for erro de quota
if (line.includes("quota") || line.includes("Quota")) {
// Marca o erro de quota no localStorage
localStorage.setItem("openai_quota_error", "true");
// Adiciona uma mensagem informativa
const infoMessage: ChatMessage = {
id: uuidv4(),
type: 'info',
content: "A API do ChatGPT está com problema de quota. Recomendações:\n1. Tente usar outro agente\n2. Configure uma nova chave API na seção de Configurações\n3. Espere até o próximo mês quando sua quota for renovada",
timestamp: new Date()
};
setMessages(prev => [...prev, errorMessage, infoMessage]);
// Exibe um toast para ajudar o usuário
toast.warn("Problema de quota na API. Consulte as recomendações no chat.", {
autoClose: 8000
});
} else {
setMessages(prev => [...prev, errorMessage]);
}
} else if (line.trim()) {
// Atualiza o conteúdo da resposta do agente
partial += line;
setMessages(prev =>
prev.map(msg =>
msg.id === agentReply.id
? { ...msg, content: partial }
: msg
)
);
}
}
if (partial.length > 200) onScrollToBottom();
// Se é um HTML válido, atualiza o editor
if (partial.includes("</html>")) {
setLatestHtml(partial);
setHtml(partial);
}
}
} catch (e: any) {
toast.error(e.message);
// Adiciona erro como mensagem
const errorMessage: ChatMessage = {
id: uuidv4(),
type: 'error',
content: e.message,
timestamp: new Date()
};
setMessages(prev => [...prev, errorMessage]);
} finally {
setisAiWorking(false);
setPrompt("");
}
};
// Função para limpar o chat
const clearChat = () => {
if (window.confirm("Tem certeza que deseja limpar todas as mensagens?")) {
setMessages([]);
toast.info("Chat limpo com sucesso");
}
};
return (
<div className="flex flex-col h-full bg-gray-950">
{/* Status bar */}
<div className="bg-gray-900 px-4 py-2 flex justify-between items-center border-b border-gray-800 text-xs text-gray-400">
<div className="flex items-center">
<span className={`w-2 h-2 rounded-full mr-2 ${isAiWorking ? 'bg-green-500 animate-pulse' : 'bg-gray-500'}`}></span>
{isAiWorking ? 'Processando...' : 'Pronto'}
</div>
<div className="flex space-x-2">
<button
onClick={clearChat}
className="text-gray-400 hover:text-gray-200 transition-colors"
title="Limpar chat"
>
<MdRefresh size={16} />
</button>
<button
className="text-gray-400 hover:text-gray-200 transition-colors"
title="Sugestões de prompt"
>
<FiInfo size={16} />
</button>
</div>
</div>
{/* Área de mensagens com scroll */}
<div className="flex-1 overflow-y-auto p-3 space-y-3">
{messages.length === 0 ? (
<div className="h-full flex flex-col items-center justify-center text-gray-500">
<FiCpu size={32} className="mb-3" />
<p className="text-sm">Inicie uma conversa com o agente {agentId}</p>
<div className="mt-4 grid grid-cols-2 gap-2">
<SuggestionButton
onClick={() => setPrompt("Crie um componente de botão com hover effect")}
text="Criar botão com hover"
/>
<SuggestionButton
onClick={() => setPrompt("Crie um formulário de contato responsivo")}
text="Formulário de contato"
/>
<SuggestionButton
onClick={() => setPrompt("Adicione um cabeçalho com menu de navegação")}
text="Cabeçalho com menu"
/>
<SuggestionButton
onClick={() => setPrompt("Implemente um dark mode toggle")}
text="Dark mode toggle"
/>
</div>
</div>
) : (
messages.map(message => (
<ChatMessageComponent key={message.id} message={message} />
))
)}
<div ref={chatEndRef} />
</div>
{/* Área de entrada */}
<div className="bg-gray-900 p-3 border-t border-gray-800">
<div className="relative">
<textarea
className={classNames(
"w-full px-4 py-3 pr-10 bg-gray-800 border border-gray-700 rounded-md resize-none text-gray-200 placeholder-gray-500 focus:outline-none focus:border-indigo-500 transition-colors",
{
"opacity-70": isAiWorking,
}
)}
rows={2}
disabled={isAiWorking}
value={prompt}
placeholder={`Pergunte algo para ${agentId}...`}
onChange={(e) => setPrompt(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
send();
}
}}
/>
<button
disabled={isAiWorking || !prompt.trim()}
onClick={send}
className={classNames(
"absolute right-3 bottom-3 p-1.5 rounded-full text-white transition-colors",
{
"bg-indigo-600 hover:bg-indigo-700": prompt.trim() && !isAiWorking,
"bg-gray-700 cursor-not-allowed": !prompt.trim() || isAiWorking
}
)}
>
<MdSend size={18} />
</button>
</div>
<div className="text-xs text-gray-500 mt-2 px-2">
Pressione Enter para enviar, Shift+Enter para nova linha
</div>
</div>
</div>
);
};
interface SuggestionButtonProps {
onClick: () => void;
text: string;
}
const SuggestionButton: React.FC<SuggestionButtonProps> = ({ onClick, text }) => (
<button
onClick={onClick}
className="px-3 py-2 bg-gray-800 hover:bg-gray-700 text-gray-300 rounded-md text-sm text-left transition-colors"
>
{text}
</button>
);
export default ChatInterface;