VocRT / frontend /src /components /ChatSidebar.tsx
Anurag
version-2 initial version
5306da4
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(); // Prevent chat selection
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;