Files
OPEN_CODEX_API/src/components/chat/ChatView.tsx
T
2026-05-16 19:29:25 -03:00

283 lines
9.7 KiB
TypeScript

import { useState, useEffect } from "react";
import { ChatHeader } from "@/components/ChatHeader";
import { ChatMessage } from "./ChatMessage";
import { ChatInput } from "./ChatInput";
import { ChatSidebar } from "./ChatSidebar";
import { ChatWelcome } from "./ChatWelcome";
import {
createWelcomeMessage,
isWelcomeMessage,
withoutWelcomeMessages,
} from "./welcome-message";
import type { Message } from "./ChatView.types";
import { ScrollArea } from "@/components/ui/scroll-area";
import { chatService, ChatRecord, MessageRecord } from "@/services/chat";
import { useToast } from "@/hooks/use-toast";
import { getModelId } from "@/config/models";
// Texto padrão da personalidade (usado para novos chats)
const DEFAULT_SYSTEM_PROMPT = "Você é um assistente útil e prestativo.";
export const ChatView = () => {
const { toast } = useToast();
const [isChatSidebarCollapsed, setIsChatSidebarCollapsed] = useState(false);
const [selectedModel, setSelectedModel] = useState("GPT-4o");
// Inicia com "0" - será atualizado com o chat_id real após primeira resposta da API
const [currentChatId, setCurrentChatId] = useState("0");
// Inicia com texto padrão - será atualizado ao carregar chat existente
const [systemPrompt, setSystemPrompt] = useState(DEFAULT_SYSTEM_PROMPT);
const [messages, setMessages] = useState<Message[]>([createWelcomeMessage()]);
const [isLoading, setIsLoading] = useState(false);
const [inputDraft, setInputDraft] = useState<{
text: string;
at: number;
} | null>(null);
const handleSelectSuggestion = (prompt: string) => {
setInputDraft({ text: prompt, at: Date.now() });
};
// NOTA: Salvamento automático desabilitado - mensagens já são salvas na API
// quando enviadas via handleSendMessage
// useEffect(() => {
// if (messages.length > 1) {
// saveCurrentChat();
// window.dispatchEvent(new Event('chatUpdated'));
// }
// }, [messages]);
// NOTA: Função de salvamento localStorage desabilitada - migrado para banco de dados
// const saveCurrentChat = () => {
// try {
// const chatTitle = chatService.generateChatTitle(
// messages.find(m => m.role === 'user')?.content || 'Nova Conversa'
// );
// const storedChat: StoredChat = {
// id: currentChatId,
// title: chatTitle,
// createdAt: new Date(),
// updatedAt: new Date(),
// model: selectedModel,
// systemPrompt: systemPrompt,
// messages: messages.map(msg => ({
// ...msg,
// timestamp: new Date(),
// })),
// };
// chatService.saveChat(storedChat);
// } catch (error) {
// console.error('Erro ao salvar chat:', error);
// }
// };
const handleNewChat = () => {
// Reseta para "0" - novo chat sempre começa com chat_id "0"
setCurrentChatId("0");
// Reseta a personalidade para o texto padrão
setSystemPrompt(DEFAULT_SYSTEM_PROMPT);
setMessages([createWelcomeMessage()]);
// Dispara evento para atualizar a sidebar
window.dispatchEvent(new Event('chatUpdated'));
};
const handleLoadChat = async (chat: ChatRecord) => {
// Carrega um chat existente do banco de dados
setCurrentChatId(chat.id);
// Atualiza a personalidade com o valor do banco de dados
// Se não tiver personalidade salva, usa o texto padrão
setSystemPrompt(chat.personalidade || DEFAULT_SYSTEM_PROMPT);
// Limpa mensagens enquanto carrega
setMessages([]);
setIsLoading(true);
try {
// Busca mensagens do chat na API
const messagesFromAPI = await chatService.getChatMessages(chat.id);
// Converte MessageRecord para Message
const loadedMessages: Message[] = messagesFromAPI.map((msg: MessageRecord) => ({
id: msg.id,
role: msg.role,
content: msg.content,
model: msg.model_name, // Nome do modelo retornado pela API
attachments: msg.has_attachments ? [] : undefined, // Não temos detalhes dos anexos no GET
}));
setMessages(loadedMessages);
toast({
title: "Chat carregado",
description: `${loadedMessages.length} mensagens carregadas.`,
});
} catch (error: any) {
console.error('Erro ao carregar mensagens:', error);
toast({
title: "Erro ao carregar chat",
description: error.message || "Não foi possível carregar as mensagens.",
variant: "destructive",
});
// Inicia com mensagem padrão em caso de erro
setMessages([createWelcomeMessage()]);
} finally {
setIsLoading(false);
}
};
const handleSendMessage = async (content: string, files?: File[]) => {
// Adiciona mensagem do usuário na interface
const newMessage: Message = {
id: Date.now().toString(),
role: "user",
content,
attachments: files?.map((file) => ({
name: file.name,
type: file.type,
size: file.size,
})),
};
setMessages((prev) => [...withoutWelcomeMessages(prev), newMessage]);
setIsLoading(true);
try {
// Envia mensagem para a API
// Converte o nome do modelo para o ID da API
const modelId = getModelId(selectedModel);
const response = await chatService.sendMessage({
chatId: currentChatId,
message: content,
modelId: modelId,
personalidade: systemPrompt,
anexos: files,
});
// Adiciona resposta da IA na interface
if (response.success) {
// IMPORTANTE: Atualiza o chat_id com o valor retornado pela API
// Isso mantém o contexto da conversa para as próximas mensagens
if (response.chat_id && response.chat_id !== currentChatId) {
console.log(`Chat ID atualizado: ${currentChatId}${response.chat_id}`);
// Se estava com chat_id "0", significa que é a primeira mensagem
// e o chat acabou de ser criado no backend
const isFirstMessage = currentChatId === "0";
setCurrentChatId(response.chat_id);
// Dispara evento para ChatSidebar recarregar e mostrar o novo chat
if (isFirstMessage) {
console.log('Primeira mensagem - novo chat criado, atualizando sidebar');
window.dispatchEvent(new Event('chatUpdated'));
}
}
const aiResponse: Message = {
id: (Date.now() + 1).toString(),
role: "assistant",
content: response.response,
model: selectedModel,
};
setMessages((prev) => [...prev, aiResponse]);
} else {
throw new Error('Resposta inválida da API');
}
} catch (error: any) {
console.error('Erro ao enviar mensagem:', error);
// Mostra toast de erro para o usuário
toast({
title: "Erro ao enviar mensagem",
description: error.message || "Não foi possível comunicar com a IA. Tente novamente.",
variant: "destructive",
});
// Adiciona mensagem de erro na interface
const errorMessage: Message = {
id: (Date.now() + 1).toString(),
role: "assistant",
content: "Desculpe, ocorreu um erro ao processar sua mensagem. Por favor, tente novamente.",
model: selectedModel,
};
setMessages((prev) => [...prev, errorMessage]);
} finally {
setIsLoading(false);
}
};
return (
<div className="flex h-full w-full pb-16 md:pb-0">
{/* Chat Sidebar - Hidden on mobile */}
<div className="hidden w-80 max-w-80 shrink-0 min-w-0 overflow-hidden md:block">
<ChatSidebar
isCollapsed={isChatSidebarCollapsed}
onToggleCollapse={() => setIsChatSidebarCollapsed(!isChatSidebarCollapsed)}
onNewChat={handleNewChat}
onSelectChat={handleLoadChat}
currentChatId={currentChatId}
/>
</div>
{/* Main Chat Area */}
<div className="flex-1 flex flex-col w-full">
<ChatHeader
showModelSelector={true}
selectedModel={selectedModel}
onModelChange={setSelectedModel}
systemPrompt={systemPrompt}
onSystemPromptChange={setSystemPrompt}
/>
<ScrollArea className="flex-1">
<div className="max-w-4xl mx-auto px-2 md:px-4">
{messages.map((message) =>
isWelcomeMessage(message) ? (
<ChatWelcome
key={message.id}
onSelectSuggestion={handleSelectSuggestion}
/>
) : (
<ChatMessage
key={message.id}
role={message.role}
content={message.content}
model={message.model}
attachments={message.attachments}
/>
)
)}
{isLoading && (
<div className="flex gap-4 py-6 px-6 bg-muted/30">
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-primary to-secondary flex items-center justify-center">
<div className="w-5 h-5 border-2 border-primary-foreground border-t-transparent rounded-full animate-spin" />
</div>
<div className="flex gap-1 items-center">
<div className="w-2 h-2 bg-primary rounded-full animate-bounce" />
<div className="w-2 h-2 bg-primary rounded-full animate-bounce delay-100" />
<div className="w-2 h-2 bg-primary rounded-full animate-bounce delay-200" />
</div>
</div>
)}
</div>
</ScrollArea>
<ChatInput
onSendMessage={handleSendMessage}
draftMessage={inputDraft?.text}
draftTrigger={inputDraft?.at}
onDraftApplied={() => setInputDraft(null)}
/>
</div>
</div>
);
};