|
import React, { useState, useEffect } from "react"; |
|
import { |
|
Plus, |
|
Search, |
|
Edit, |
|
Trash, |
|
List, |
|
ArrowLeft, |
|
ArrowRight, |
|
Loader2, |
|
} from "lucide-react"; |
|
import { Button } from "@/components/ui/button"; |
|
import { v4 as uuidv4 } from "uuid"; |
|
import { Input } from "@/components/ui/input"; |
|
import { useAppDispatch, useAppSelector } from "@/redux/hooks"; |
|
import { |
|
createChat, |
|
setActiveChat, |
|
deleteChat, |
|
setSearchQuery, |
|
renameChat, |
|
} from "@/redux/slices/chatsSlice"; |
|
import { |
|
addMessage, |
|
setActiveMessages, |
|
setActiveContext, |
|
setSidebarVisibility, |
|
clearEmbeddings, |
|
clearActiveMessages, |
|
toggleSidebar, |
|
setActiveChatPrompt, |
|
} from "@/redux/slices/chatSlice"; |
|
import { useToast } from "@/hooks/use-toast"; |
|
import { resetSessionId, setSessionId } from "@/redux/slices/sessionSlice"; |
|
import { useParams } from "react-router-dom"; |
|
import api from "@/api/apiClient"; |
|
|
|
interface ChatSidebarProps { |
|
isMobileSidebar?: boolean; |
|
onClose?: () => void; |
|
} |
|
|
|
const ChatSidebar: React.FC<ChatSidebarProps> = ({ |
|
isMobileSidebar = false, |
|
onClose, |
|
}) => { |
|
const dispatch = useAppDispatch(); |
|
const { chats, activeChat, searchQuery } = useAppSelector( |
|
(state) => state.chats |
|
); |
|
const { isListening, uploadingEmbeddings } = useAppSelector( |
|
(state) => state.chat |
|
); |
|
|
|
const [isRenaming, setIsRenaming] = useState<string | null>(null); |
|
const [deleteChatIndex, setDeletingChatIndex] = useState<number>(-1); |
|
const [newTitle, setNewTitle] = useState(""); |
|
const { toast } = useToast(); |
|
|
|
if (!chats) { |
|
return ( |
|
<div> |
|
<Loader2 className="animate-spin text-white" /> |
|
</div> |
|
); |
|
} |
|
|
|
const filteredChats = chats.filter((chat) => |
|
chat.title.toLowerCase().includes(searchQuery.toLowerCase()) |
|
); |
|
|
|
const handleCreateNewChat = () => { |
|
const newSeesionId = uuidv4(); |
|
dispatch(resetSessionId(newSeesionId)); |
|
dispatch(createChat(newSeesionId)); |
|
dispatch(setActiveMessages([])); |
|
dispatch(setActiveContext([])); |
|
dispatch(setActiveChat(newSeesionId)); |
|
dispatch(setActiveChatPrompt("")); |
|
|
|
toast({ |
|
title: "New chat created", |
|
description: "Start a fresh conversation with your AI assistant.", |
|
}); |
|
|
|
if (isMobileSidebar && onClose) { |
|
onClose(); |
|
} |
|
}; |
|
|
|
const handleSelectChat = async (chatId: string) => { |
|
if (isListening) { |
|
return; |
|
} |
|
if (chatId !== activeChat) { |
|
dispatch(setActiveChat(chatId)); |
|
const activeChats = chats.find((item) => item.id == chatId); |
|
dispatch(setActiveMessages(activeChats.chat)); |
|
dispatch(setActiveContext(activeChats.context)); |
|
dispatch(setActiveChatPrompt(activeChats?.prompt || "")); |
|
dispatch(setSessionId(chatId)); |
|
if (isMobileSidebar && onClose) { |
|
onClose(); |
|
} |
|
} |
|
}; |
|
|
|
const handleStartRename = ( |
|
chatId: string, |
|
currentTitle: string, |
|
event: React.MouseEvent |
|
) => { |
|
event.stopPropagation(); |
|
setIsRenaming(chatId); |
|
setNewTitle(currentTitle); |
|
}; |
|
|
|
const handleRenameChat = async (chatId: string) => { |
|
try { |
|
if (newTitle.trim()) { |
|
await dispatch( |
|
renameChat({ sessionId: chatId, title: newTitle.trim() }) |
|
).unwrap(); |
|
toast({ |
|
title: "Chat renamed", |
|
description: "Your chat has been renamed successfully.", |
|
}); |
|
} |
|
} catch (error) { |
|
console.error(error); |
|
} finally { |
|
setIsRenaming(null); |
|
} |
|
}; |
|
|
|
const handleCloseSidebar = () => { |
|
if (isMobileSidebar && onClose) { |
|
onClose(); |
|
} else { |
|
dispatch(setSidebarVisibility(false)); |
|
} |
|
}; |
|
|
|
const handleClearChat = async (index: number, chatId: string) => { |
|
try { |
|
setDeletingChatIndex(index); |
|
const response = await api.post("/chat/delete-chat", { |
|
sessionId: chatId, |
|
}); |
|
console.log(response.data); |
|
|
|
if (response.success) { |
|
dispatch(deleteChat(chatId)); |
|
dispatch(clearEmbeddings()); |
|
dispatch(clearActiveMessages()); |
|
toast({ |
|
title: "Chat deleted", |
|
description: "The chat has been removed from your history.", |
|
}); |
|
} |
|
} catch (error) { |
|
toast({ |
|
title: "Error", |
|
description: "failed to delete chat.", |
|
variant: "destructive", |
|
}); |
|
} finally { |
|
setDeletingChatIndex(-1); |
|
} |
|
}; |
|
|
|
return ( |
|
<div |
|
className={`flex pt-5 flex-col h-full bg-black border-r border-white/10 ${ |
|
isMobileSidebar ? "w-full" : "w-64" |
|
}`} |
|
> |
|
<div className="flex items-center justify-between p-4 border-b border-white/10"> |
|
<Button |
|
onClick={handleCreateNewChat} |
|
className="bg-white text-black hover:bg-white/90 flex items-center justify-center gap-2 flex-1 mr-2" |
|
> |
|
<Plus size={16} /> New Chat |
|
</Button> |
|
<Button |
|
variant="ghost" |
|
size="icon" |
|
onClick={handleCloseSidebar} |
|
className="h-9 w-9 text-white/70 hover:text-gray-500" |
|
> |
|
<ArrowLeft size={18} /> |
|
</Button> |
|
</div> |
|
|
|
<div className="p-4 border-b border-white/10"> |
|
<div className="relative"> |
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-white/50 h-4 w-4" /> |
|
<Input |
|
className="pl-10 bg-white/10 border-white/20 text-white placeholder:text-white/50 focus:border-white/30" |
|
placeholder="Search chats..." |
|
value={searchQuery} |
|
onChange={(e) => dispatch(setSearchQuery(e.target.value))} |
|
/> |
|
</div> |
|
</div> |
|
|
|
<div className="flex-1 overflow-y-auto p-2"> |
|
{filteredChats.length === 0 ? ( |
|
<div className="text-center text-white/50 p-4">No chats found</div> |
|
) : ( |
|
filteredChats.map((chat, index) => ( |
|
<div |
|
key={chat.id} |
|
className={`flex items-center justify-between p-3 mb-1 rounded-lg cursor-pointer group transition-all ${ |
|
chat.id === activeChat |
|
? "bg-white/20 text-white" |
|
: "hover:bg-white/10 text-white/80" |
|
}`} |
|
onClick={() => { |
|
handleSelectChat(chat.id); |
|
}} |
|
> |
|
{isRenaming === chat.id ? ( |
|
<div |
|
className="flex-1 flex" |
|
onClick={(e) => e.stopPropagation()} |
|
> |
|
<Input |
|
className="flex-1 bg-black border-white/30 text-white" |
|
value={newTitle} |
|
onChange={(e) => setNewTitle(e.target.value)} |
|
onKeyDown={(e) => { |
|
if (e.key === "Enter") { |
|
handleRenameChat(chat.id); |
|
} else if (e.key === "Escape") { |
|
setIsRenaming(null); |
|
} |
|
}} |
|
autoFocus |
|
/> |
|
<Button |
|
variant="ghost" |
|
size="sm" |
|
onClick={(e) => { |
|
e.stopPropagation(); |
|
handleRenameChat(chat.id); |
|
}} |
|
className="ml-1" |
|
> |
|
<ArrowRight size={16} /> |
|
</Button> |
|
</div> |
|
) : ( |
|
<> |
|
<div className="flex-1 truncate mr-2"> |
|
<div className="flex items-center"> |
|
<List className="h-4 w-4 mr-2 opacity-70" /> |
|
<span className="truncate">{chat.title}</span> |
|
</div> |
|
<div className="text-xs opacity-50 mt-1"> |
|
{new Date(chat.lastUpdatedAt).toLocaleDateString()} |
|
</div> |
|
</div> |
|
<div className="flex opacity-0 group-hover:opacity-100 transition-opacity"> |
|
<Button |
|
variant="ghost" |
|
size="icon" |
|
className="h-8 w-8" |
|
onClick={(e) => handleStartRename(chat.id, chat.title, e)} |
|
> |
|
<Edit className="h-4 w-4" /> |
|
</Button> |
|
<Button |
|
variant="ghost" |
|
size="icon" |
|
className="relative h-8 w-8 text-red-400 hover:text-red-300" |
|
onClick={(e) => { |
|
e.stopPropagation(); |
|
handleClearChat(index, chat.id); |
|
}} |
|
disabled={deleteChatIndex !== -1} |
|
> |
|
{deleteChatIndex !== -1 && deleteChatIndex === index ? ( |
|
<Loader2 className="animate-spin h-4 w-4" /> |
|
) : ( |
|
<Trash className="h-4 w-4" /> |
|
)} |
|
</Button> |
|
</div> |
|
</> |
|
)} |
|
</div> |
|
)) |
|
)} |
|
</div> |
|
</div> |
|
); |
|
}; |
|
|
|
export default ChatSidebar; |
|
|