283 lines
9.7 KiB
TypeScript
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>
|
|
);
|
|
};
|