|
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); |
|
|
|
|
|
useEffect(() => { |
|
chatEndRef.current?.scrollIntoView({ behavior: 'smooth' }); |
|
}, [messages]); |
|
|
|
|
|
const send = async () => { |
|
if (!prompt.trim() || isAiWorking) return; |
|
|
|
setisAiWorking(true); |
|
onNewPrompt(prompt); |
|
|
|
|
|
const userMessage: ChatMessage = { |
|
id: uuidv4(), |
|
type: 'user', |
|
content: prompt, |
|
timestamp: new Date() |
|
}; |
|
setMessages(prev => [...prev, userMessage]); |
|
|
|
try { |
|
|
|
const context = { |
|
currentHtml: html !== defaultHTML ? html : "Sem HTML ainda", |
|
previousMessages: messages.slice(-5), |
|
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) |
|
}), |
|
}); |
|
|
|
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() |
|
}; |
|
|
|
|
|
setMessages(prev => [...prev, agentReply]); |
|
|
|
while (true) { |
|
const { done, value } = await reader!.read(); |
|
if (done) break; |
|
const chunk = dec.decode(value); |
|
|
|
|
|
const lines = chunk.split('\n'); |
|
for (const line of lines) { |
|
if (line.trim().startsWith('[LOG]')) { |
|
|
|
const logMessage: ChatMessage = { |
|
id: uuidv4(), |
|
type: 'log', |
|
content: line.replace('[LOG] ', ''), |
|
timestamp: new Date() |
|
}; |
|
setMessages(prev => [...prev, logMessage]); |
|
} else if (line.trim().startsWith('❌')) { |
|
|
|
const errorMessage: ChatMessage = { |
|
id: uuidv4(), |
|
type: 'error', |
|
content: line, |
|
timestamp: new Date() |
|
}; |
|
|
|
|
|
if (line.includes("quota") || line.includes("Quota")) { |
|
|
|
localStorage.setItem("openai_quota_error", "true"); |
|
|
|
|
|
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]); |
|
|
|
|
|
toast.warn("Problema de quota na API. Consulte as recomendações no chat.", { |
|
autoClose: 8000 |
|
}); |
|
} else { |
|
setMessages(prev => [...prev, errorMessage]); |
|
} |
|
} else if (line.trim()) { |
|
|
|
partial += line; |
|
setMessages(prev => |
|
prev.map(msg => |
|
msg.id === agentReply.id |
|
? { ...msg, content: partial } |
|
: msg |
|
) |
|
); |
|
} |
|
} |
|
|
|
if (partial.length > 200) onScrollToBottom(); |
|
|
|
|
|
if (partial.includes("</html>")) { |
|
setLatestHtml(partial); |
|
setHtml(partial); |
|
} |
|
} |
|
} catch (e: any) { |
|
toast.error(e.message); |
|
|
|
const errorMessage: ChatMessage = { |
|
id: uuidv4(), |
|
type: 'error', |
|
content: e.message, |
|
timestamp: new Date() |
|
}; |
|
setMessages(prev => [...prev, errorMessage]); |
|
} finally { |
|
setisAiWorking(false); |
|
setPrompt(""); |
|
} |
|
}; |
|
|
|
|
|
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; |