Spaces:
Sleeping
Sleeping
| import React from "react"; | |
| import { format } from "date-fns"; | |
| import { Bot, TrashIcon, User, FileText, Settings, PanelLeft, Plus } from "lucide-react"; | |
| import { Button } from "@/components/ui/button"; | |
| import { cn } from "@/lib/utils"; | |
| import { Chat } from "@/types/chat"; | |
| import { ModeToggle } from "@/components/layout/ModeToggle"; | |
| interface ChatSidebarProps { | |
| chats: Chat[]; | |
| activeChat: Chat | null; | |
| isGeneratingTitle: boolean; | |
| createNewChat: () => void; | |
| selectChat: (id: string) => void; | |
| onRequestDelete: (id: string) => void; | |
| onOpenSettings: () => void; | |
| onOpenSources: () => void; | |
| openProfileModal: () => void; | |
| isSidebarOpen: boolean; | |
| setIsSidebarOpen: (open: boolean) => void; | |
| } | |
| export const ChatSidebar: React.FC<ChatSidebarProps> = ({ | |
| chats, | |
| activeChat, | |
| isGeneratingTitle, | |
| createNewChat, | |
| selectChat, | |
| onRequestDelete, | |
| onOpenSettings, | |
| onOpenSources, | |
| openProfileModal, | |
| isSidebarOpen, | |
| setIsSidebarOpen, | |
| }) => { | |
| return ( | |
| <div className={cn( | |
| "fixed top-0 bottom-0 left-0 z-20 bg-background/90 backdrop-blur-lg", | |
| "transition-transform duration-300 ease-in-out", | |
| isSidebarOpen ? 'translate-x-0' : '-translate-x-full md:translate-x-0', | |
| "w-72 lg:w-80 border-r border-border/50 flex-shrink-0", | |
| "md:relative md:inset-auto h-full md:z-0" | |
| )}> | |
| <div className="flex flex-col h-full"> | |
| <div className="p-4 pb-0"> | |
| <div className="flex justify-between items-center"> | |
| <div className="flex items-center space-x-2"> | |
| <div className="h-8 w-8 bg-primary/90 rounded-md flex items-center justify-center"> | |
| <span className="text-white font-bold text-lg">AI</span> | |
| </div> | |
| <h1 className="text-xl font-semibold"> | |
| Insight AI | |
| </h1> | |
| </div> | |
| <Button className="md:hidden" variant="ghost" size="icon" onClick={() => setIsSidebarOpen(false)}> | |
| <PanelLeft className="h-5 w-5" /> | |
| </Button> | |
| </div> | |
| <div className="py-4"> | |
| <Button | |
| onClick={createNewChat} | |
| className="w-full justify-start gap-2" | |
| variant="outline" | |
| > | |
| <Plus className="h-4 w-4" /> | |
| New Chat | |
| </Button> | |
| </div> | |
| <div className="flex items-center justify-between"> | |
| <div className="flex items-center gap-2"> | |
| <Bot className="h-5 w-5 text-primary" /> | |
| <span className="font-medium">Recent Chats</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div className="flex-1 overflow-y-auto p-2 space-y-1 scrollbar-thin"> | |
| {chats.length === 0 ? ( | |
| <div className="text-center text-muted-foreground p-4"> | |
| No conversations yet. Start a new one! | |
| </div> | |
| ) : ( | |
| chats.map(chat => ( | |
| <div | |
| key={chat.id} | |
| onClick={() => selectChat(chat.id)} | |
| className={cn( | |
| "flex items-center justify-between p-2 px-3 rounded-lg cursor-pointer group transition-all", | |
| activeChat?.id === chat.id | |
| ? "bg-primary/10 border border-primary/20" | |
| : "hover:bg-muted/50 border border-transparent" | |
| )} | |
| > | |
| <div className="flex-1 truncate"> | |
| <div className={cn( | |
| "font-medium truncate flex items-center", | |
| activeChat?.id === chat.id && "text-primary" | |
| )}> | |
| <Bot className="h-3.5 w-3.5 mr-1.5 opacity-70" /> | |
| {chat.title} | |
| {chat.id === activeChat?.id && isGeneratingTitle && ( | |
| <span className="ml-1.5 inline-block h-2 w-2 rounded-full bg-primary/70 animate-pulse"></span> | |
| )} | |
| </div> | |
| <div className="text-xs text-muted-foreground"> | |
| {chat.messages.filter(m => m.sender === "user").length} messages • {format(new Date(chat.updatedAt), "MMM d")} | |
| </div> | |
| </div> | |
| <Button | |
| variant="ghost" | |
| size="icon" | |
| className="h-7 w-7 opacity-0 group-hover:opacity-100 transition-opacity hover:bg-destructive/10 hover:text-destructive" | |
| onClick={(e) => { | |
| e.stopPropagation(); | |
| onRequestDelete(chat.id); | |
| }} | |
| > | |
| <TrashIcon className="h-3.5 w-3.5" /> | |
| </Button> | |
| </div> | |
| )) | |
| )} | |
| </div> | |
| {/* Sidebar Footer */} | |
| <div className="p-3 space-y-2"> | |
| <Button | |
| onClick={openProfileModal} | |
| variant="ghost" | |
| className="w-full justify-start gap-2 text-muted-foreground hover:text-foreground" | |
| size="sm" | |
| > | |
| <User className="h-4 w-4" /> | |
| Profile | |
| </Button> | |
| <Button | |
| onClick={onOpenSources} | |
| variant="ghost" | |
| className="w-full justify-start gap-2 text-muted-foreground hover:text-foreground" | |
| size="sm" | |
| > | |
| <FileText className="h-4 w-4" /> | |
| View Sources | |
| </Button> | |
| <Button | |
| onClick={onOpenSettings} | |
| variant="ghost" | |
| className="w-full justify-start gap-2 text-muted-foreground hover:text-foreground" | |
| size="sm" | |
| > | |
| <Settings className="h-4 w-4" /> | |
| Settings | |
| </Button> | |
| <div className="flex items-center justify-between pt-2 border-t"> | |
| <span className="text-xs text-muted-foreground">Theme</span> | |
| <ModeToggle /> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| }; | |