import { useState, useRef, useEffect } from "react"; import { Send, Plus, PanelLeft, Bot } from "lucide-react"; import { Button } from "@/components/ui/button"; import { apiService, Message as APIMessage } from "@/services/apiService"; import { toast } from "@/components/ui/sonner"; import { ChatBubble } from "@/components/chat/ChatBubble"; import { Separator } from "@/components/ui/separator"; import { cn } from "@/lib/utils"; import { storage, STORAGE_KEYS } from "@/lib/storage"; import { Message, Chat } from "@/types/chat"; import { ProfileModal } from "../modals/ProfileModal"; import { ChatSidebar } from "./ChatSidebar"; import { ChatInputArea } from "./ChatInputArea"; import { WelcomeScreen } from "./WelcomeScreen"; import { DeleteChatDialog } from "./DeleteChatDialog"; interface ChatInterfaceProps { onOpenSettings: () => void; onOpenSources: () => void; } const WELCOME_MESSAGE = "Hello! I'm Insight AI, How can I help you today?"; const generateId = () => Math.random().toString(36).substring(2, 11); export const ChatInterface = ({ onOpenSettings, onOpenSources }: ChatInterfaceProps) => { const [chats, setChats] = useState(() => { const savedChats = storage.get(STORAGE_KEYS.CHATS); if (savedChats) { try { return savedChats.map((chat: any) => ({ ...chat, messages: chat.messages.map((msg: any) => ({ ...msg, timestamp: new Date(msg.timestamp), sender: msg.sender as "user" | "system" })), createdAt: new Date(chat.createdAt), updatedAt: new Date(chat.updatedAt) })); } catch (error) { console.error("Failed to parse saved chats:", error); return []; } } return []; }); const [activeChat, setActiveChat] = useState(() => { if (chats.length > 0) { return chats[0]; } }); const [inputValue, setInputValue] = useState(""); const [isLoading, setIsLoading] = useState(false); const [isSidebarOpen, setIsSidebarOpen] = useState(false); const [isGeneratingTitle, setIsGeneratingTitle] = useState(false); const [isProfileModalOpen, setIsProfileModalOpen] = useState(false); const [chatToDelete, setChatToDelete] = useState(null); const messagesEndRef = useRef(null); const inputRef = useRef(null); useEffect(() => { // Save chats to storage whenever they change if (activeChat && !chats.find(chat => chat.id === activeChat.id)) { setChats([activeChat, ...chats]); } const allChats = activeChat ? [ activeChat, ...chats.filter(chat => chat.id !== activeChat.id) ] : chats; storage.set(STORAGE_KEYS.CHATS, allChats); }, [chats, activeChat]); const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }; useEffect(() => { scrollToBottom(); }, [activeChat?.messages]); useEffect(() => { // Focus input when component mounts or when loading ends if (!isLoading) { setTimeout(() => { inputRef.current?.focus(); }, 100); } }, [isLoading]); // Handle new chat and chat selection events useEffect(() => { const handleNewChat = () => createNewChat(); const handleSelectChat = (e: Event) => { const customEvent = e as CustomEvent; const chatId = customEvent.detail?.chatId; if (chatId) { selectChat(chatId); } }; const handleDeleteChat = (e: Event) => { const customEvent = e as CustomEvent; const chatId = customEvent.detail?.chatId; if (chatId) { setChatToDelete(chatId); } }; document.addEventListener("insight:new-chat", handleNewChat); document.addEventListener("insight:select-chat", handleSelectChat); document.addEventListener("insight:delete-chat", handleDeleteChat); return () => { document.removeEventListener("insight:new-chat", handleNewChat); document.removeEventListener("insight:select-chat", handleSelectChat); document.removeEventListener("insight:delete-chat", handleDeleteChat); }; }, [chats, activeChat]); const generateChatTitle = async (query: string) => { if (!activeChat || activeChat.title !== "New Chat") return; setIsGeneratingTitle(true); try { const response = await apiService.generateTitle(query); if (response.title) { // Update the active chat with the new title setActiveChat(prevChat => { if (!prevChat) return null; const updatedChat = { ...prevChat, title: response.title }; // Update chats list setChats(prevChats => prevChats.map(chat => chat.id === updatedChat.id ? updatedChat : chat ) ); return updatedChat; }); } } catch (error) { console.error("Error generating chat title:", error); // Fallback to using query as title if (activeChat.title === "New Chat") { setActiveChat(prevChat => { if (!prevChat) return null; const updatedChat = { ...prevChat, title: query.slice(0, 30) + (query.length > 30 ? '...' : '') }; setChats(prevChats => prevChats.map(chat => chat.id === updatedChat.id ? updatedChat : chat ) ); return updatedChat; }); } } finally { setIsGeneratingTitle(false); } }; const handleSendMessage = async (e?: React.FormEvent) => { if (e) e.preventDefault(); if (!inputValue.trim() || !activeChat) return; const userMessage: Message = { id: generateId(), content: inputValue, sender: "user", timestamp: new Date() }; const loadingMessage: Message = { id: generateId(), content: "", sender: "system", timestamp: new Date(), isLoading: true }; // Update active chat with new messages const updatedChat = { ...activeChat, messages: [...activeChat.messages, userMessage, loadingMessage], updatedAt: new Date() }; setActiveChat(updatedChat); setInputValue(""); setIsLoading(true); try { // Prepare chat history for the API const chatHistory: APIMessage[] = updatedChat.messages .filter(msg => !msg.isLoading && msg.content) // filter out loading messages .slice(0, -1) // exclude the loading message we just added .map(msg => ({ role: msg.sender === "user" ? "user" : "assistant", content: msg.content })); const response = await apiService.queryRulings({ query: userMessage.content, chat_history: chatHistory }); // Replace loading message with actual response const updatedMessages = updatedChat.messages.map(msg => msg.id === loadingMessage.id ? { ...msg, content: response.answer, isLoading: false, result: response.retrieved_sources } : msg ); const finalChat = { ...updatedChat, messages: updatedMessages, }; setActiveChat(finalChat); // Update chats list setChats(prevChats => prevChats.map(chat => chat.id === finalChat.id ? finalChat : chat ) ); // Generate title if this is a new chat if (updatedChat.title === "New Chat" && updatedChat.messages.length <= 3) { generateChatTitle(userMessage.content); } } catch (error) { console.error("Error querying AI:", error); // Replace loading message with error const updatedMessages = updatedChat.messages.map(msg => msg.id === loadingMessage.id ? { ...msg, content: "I'm sorry, I couldn't process your request. Please try again.", isLoading: false, error: true } : msg ); setActiveChat({ ...updatedChat, messages: updatedMessages }); toast.error("Failed to process your request"); } finally { setIsLoading(false); // Refocus the input after sending message setTimeout(() => { inputRef.current?.focus(); }, 100); } }; const createNewChat = () => { const newChat: Chat = { id: generateId(), title: "New Chat", messages: [ { id: "welcomems", content: WELCOME_MESSAGE, sender: "system" as const, timestamp: new Date() } ], createdAt: new Date(), updatedAt: new Date() }; setActiveChat(newChat); setChats(prev => [newChat, ...prev]); setTimeout(() => { inputRef.current?.focus(); }, 100); setIsSidebarOpen(false); }; const selectChat = (chatId: string) => { const selectedChat = chats.find(chat => chat.id === chatId); if (selectedChat) { setActiveChat(selectedChat); setIsSidebarOpen(false); setTimeout(() => { inputRef.current?.focus(); }, 100); } }; const deleteChat = (chatId: string) => { const updatedChats = chats.filter(chat => chat.id !== chatId); setChats(updatedChats); // If we're deleting the active chat, switch to another one if (activeChat?.id === chatId) { setActiveChat(updatedChats.length > 0 ? updatedChats[0] : null); // If no chats left, create a new one if (updatedChats.length === 0) { createNewChat(); } } // Clear the chat being deleted setChatToDelete(null); }; const handleDeleteMessage = (messageId: string) => { if (!activeChat) return; // Find the index of the message to delete const messageIndex = activeChat.messages.findIndex(msg => msg.id === messageId); if (messageIndex === -1) return; // Determine if we need to delete a pair (user message + assistant response) const isUserMessage = activeChat.messages[messageIndex].sender === "user"; const updatedMessages = [...activeChat.messages]; if (isUserMessage && messageIndex + 1 < updatedMessages.length && updatedMessages[messageIndex + 1].sender === "system") { // Remove both the user message and the following assistant response updatedMessages.splice(messageIndex, 2); } else if (!isUserMessage && messageIndex > 0 && updatedMessages[messageIndex - 1].sender === "user") { // Remove both the assistant message and the preceding user message updatedMessages.splice(messageIndex - 1, 2); } else { // Just remove the single message updatedMessages.splice(messageIndex, 1); } const updatedChat = { ...activeChat, messages: updatedMessages, updatedAt: new Date() }; setActiveChat(updatedChat); // Update chats list setChats(prevChats => prevChats.map(chat => chat.id === updatedChat.id ? updatedChat : chat ) ); }; const handleRegenerateMessage = (messageId: string) => { if (!activeChat) return; // Find the system message that needs to be regenerated const messageIndex = activeChat.messages.findIndex( msg => msg.id === messageId && msg.sender === "system" ); if (messageIndex < 0) return; const message = activeChat.messages[messageIndex]; // Find the last user message before this system message let userMessageContent = ""; let userMessageIndex = -1; for (let i = messageIndex - 1; i >= 0; i--) { if (activeChat.messages[i].sender === "user") { userMessageContent = activeChat.messages[i].content; userMessageIndex = i; break; } } if (!userMessageContent) return; // Create a new variation const variationId = generateId(); const now = new Date(); // Prepare the updated message with a loading variation const variations = message.variations || []; const existingVariations = variations.map(v => ({ ...v })); // Create a new variations array with the loading state const updatedVariations = [ ...existingVariations, { id: variationId, content: "", timestamp: now } ]; // Update the message with loading state const updatedMessages = [...activeChat.messages]; updatedMessages[messageIndex] = { ...updatedMessages[messageIndex], isLoading: true, variations: updatedVariations, activeVariation: variationId }; const updatedChat = { ...activeChat, messages: updatedMessages }; setActiveChat(updatedChat); setIsLoading(true); // Prepare chat history for the API // Include only the messages up to the user message that triggered the original response const chatHistory: APIMessage[] = activeChat.messages .slice(0, userMessageIndex) .filter(msg => !msg.isLoading && msg.content) .map(msg => ({ role: msg.sender === "user" ? "user" : "assistant", content: msg.content })); // Send the API request apiService.queryRulings({ query: userMessageContent, chat_history: chatHistory }) .then(response => { // Update the variation with the actual response const finalVariations = updatedMessages[messageIndex].variations!.map(v => v.id === variationId ? { ...v, content: response.answer, timestamp: new Date() } : v ); const finalMessages = [...updatedMessages]; finalMessages[messageIndex] = { ...finalMessages[messageIndex], variations: finalVariations, isLoading: false, error: false, result: response.retrieved_sources, activeVariation: variationId }; const finalChat = { ...updatedChat, messages: finalMessages }; setActiveChat(finalChat); setChats(prevChats => prevChats.map(chat => chat.id === finalChat.id ? finalChat : chat ) ); }) .catch(error => { console.error("Error regenerating response:", error); // Remove the failed variation const finalVariations = updatedMessages[messageIndex].variations!.filter(v => v.id !== variationId ); const finalMessages = [...updatedMessages]; finalMessages[messageIndex] = { ...finalMessages[messageIndex], variations: finalVariations, isLoading: false, activeVariation: finalVariations.length > 0 ? finalVariations[0].id : undefined }; setActiveChat({ ...updatedChat, messages: finalMessages }); toast.error("Failed to generate variation"); }) .finally(() => { setIsLoading(false); setTimeout(() => { inputRef.current?.focus(); }, 100); }); }; const handleSelectVariation = (messageId: string, variationId: string) => { if (!activeChat) return; // Find the message const messageIndex = activeChat.messages.findIndex(msg => msg.id === messageId); if (messageIndex < 0) return; // Set the active variation const updatedMessages = [...activeChat.messages]; updatedMessages[messageIndex] = { ...updatedMessages[messageIndex], activeVariation: variationId }; const updatedChat = { ...activeChat, messages: updatedMessages }; setActiveChat(updatedChat); // Update chats list setChats(prevChats => prevChats.map(chat => chat.id === updatedChat.id ? updatedChat : chat ) ); }; const handleRetryMessage = (messageId: string) => { if (!activeChat) return; // Find the failed message const failedMessageIndex = activeChat.messages.findIndex( msg => msg.id === messageId && msg.error ); if (failedMessageIndex < 0) return; // Get the last user message before this failed message let userMessageContent = ""; for (let i = failedMessageIndex - 1; i >= 0; i--) { if (activeChat.messages[i].sender === "user") { userMessageContent = activeChat.messages[i].content; break; } } if (!userMessageContent) return; // Remove the failed message const updatedMessages = [...activeChat.messages]; updatedMessages[failedMessageIndex] = { ...updatedMessages[failedMessageIndex], isLoading: true, error: false, content: "" }; const updatedChat = { ...activeChat, messages: updatedMessages }; setActiveChat(updatedChat); setIsLoading(true); // Prepare chat history for the API const chatHistory: APIMessage[] = updatedChat.messages .filter(msg => !msg.isLoading && msg.content && updatedChat.messages.indexOf(msg) < failedMessageIndex - 1) .map(msg => ({ role: msg.sender === "user" ? "user" : "assistant", content: msg.content })); // Retry the query apiService.queryRulings({ query: userMessageContent, chat_history: chatHistory }) .then(response => { const finalMessages = [...updatedMessages]; finalMessages[failedMessageIndex] = { ...finalMessages[failedMessageIndex], content: response.answer, isLoading: false, error: false, result: response.retrieved_sources }; const finalChat = { ...updatedChat, messages: finalMessages }; setActiveChat(finalChat); setChats(prevChats => prevChats.map(chat => chat.id === finalChat.id ? finalChat : chat ) ); }) .catch(error => { console.error("Error retrying query:", error); const finalMessages = [...updatedMessages]; finalMessages[failedMessageIndex] = { ...finalMessages[failedMessageIndex], content: "I'm sorry, I couldn't process your request. Please try again.", isLoading: false, error: true }; setActiveChat({ ...updatedChat, messages: finalMessages }); toast.error("Failed to process your request"); }) .finally(() => { setIsLoading(false); setTimeout(() => { inputRef.current?.focus(); }, 100); }); }; const openProfileModal = () => { setIsProfileModalOpen(true); }; return ( <>
{/* Chat Sidebar */} {/* Chat Main Area */}
{/* Mobile Header */}
{activeChat?.title || "New Chat"}
{/* Chat Area */}
{!activeChat ? ( ) : ( <>
{activeChat.messages.map((message) => ( ))}
{/* Input Area */} )}
{/* Profile Modal */} setIsProfileModalOpen(false)} /> {/* Delete Chat Confirmation Dialog */} setChatToDelete(null)} onDelete={() => chatToDelete && deleteChat(chatToDelete)} /> ); };