scira-chat / components /chat-sidebar.tsx
mukaddamzaid's picture
feat: enhanced UI
7984c85
raw
history blame
19.3 kB
"use client";
import { useState, useEffect, useRef } from "react";
import { useRouter, usePathname } from "next/navigation";
import { MessageSquare, PlusCircle, Trash2, ServerIcon, Settings, Loader2, Sparkles, ChevronsUpDown, UserIcon, Copy } from "lucide-react";
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuBadge,
useSidebar
} from "@/components/ui/sidebar";
import { Separator } from "@/components/ui/separator";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { toast } from "sonner";
import Image from "next/image";
import { MCPServerManager, type MCPServer } from "./mcp-server-manager";
import { ThemeToggle } from "./theme-toggle";
import { getUserId } from "@/lib/user-id";
import { useLocalStorage } from "@/lib/hooks/use-local-storage";
import { STORAGE_KEYS } from "@/lib/constants";
import { useChats } from "@/lib/hooks/use-chats";
import { cn } from "@/lib/utils";
import Link from "next/link";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
export function ChatSidebar() {
const router = useRouter();
const pathname = usePathname();
const [userId, setUserId] = useState<string>('');
const [mcpServers, setMcpServers] = useLocalStorage<MCPServer[]>(STORAGE_KEYS.MCP_SERVERS, []);
const [selectedMcpServers, setSelectedMcpServers] = useLocalStorage<string[]>(STORAGE_KEYS.SELECTED_MCP_SERVERS, []);
const [mcpSettingsOpen, setMcpSettingsOpen] = useState(false);
const { state } = useSidebar();
const isCollapsed = state === "collapsed";
// Initialize userId
useEffect(() => {
setUserId(getUserId());
}, []);
// Use TanStack Query to fetch chats
const { chats, isLoading, deleteChat, refreshChats } = useChats(userId);
// Start a new chat
const handleNewChat = () => {
router.push('/');
};
// Delete a chat
const handleDeleteChat = async (chatId: string, e: React.MouseEvent) => {
e.stopPropagation();
e.preventDefault();
deleteChat(chatId);
// If we're currently on the deleted chat's page, navigate to home
if (pathname === `/chat/${chatId}`) {
router.push('/');
}
};
// Get active MCP servers status
const activeServersCount = selectedMcpServers.length;
// Show loading state if user ID is not yet initialized
if (!userId) {
return null; // Or a loading spinner
}
return (
<Sidebar className="shadow-sm bg-background/80 dark:bg-background/40 backdrop-blur-md" collapsible="icon">
<SidebarHeader className="p-4 border-b border-border/40">
<div className="flex items-center justify-start">
<div className={`flex items-center gap-2 ${isCollapsed ? "justify-center w-full" : ""}`}>
<div className={`relative rounded-full bg-primary/10 flex items-center justify-center ${isCollapsed ? "size-5 p-3" : "size-6"}`}>
<Image src="/scira.png" alt="Scira Logo" width={24} height={24} className="invert dark:invert-0 absolute transform scale-75" unoptimized quality={100} />
</div>
{!isCollapsed && (
<div className="font-semibold text-lg text-foreground/90">MCP</div>
)}
</div>
</div>
</SidebarHeader>
<SidebarContent className="flex flex-col h-[calc(100vh-8rem)]">
<SidebarGroup className="flex-1 min-h-0">
<SidebarGroupLabel className={cn(
"px-4 text-xs font-medium text-muted-foreground/80 uppercase tracking-wider",
isCollapsed ? "sr-only" : ""
)}>
Chats
</SidebarGroupLabel>
<SidebarGroupContent className="overflow-y-auto pt-1">
<SidebarMenu>
{isLoading ? (
<div className={`flex items-center justify-center py-4 ${isCollapsed ? "" : "px-4"}`}>
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
{!isCollapsed && (
<span className="ml-2 text-xs text-muted-foreground">Loading...</span>
)}
</div>
) : chats.length === 0 ? (
<div className={`flex items-center justify-center py-3 ${isCollapsed ? "" : "px-4"}`}>
{isCollapsed ? (
<div className="flex h-6 w-6 items-center justify-center rounded-md border border-border/50 bg-background/50">
<MessageSquare className="h-3 w-3 text-muted-foreground" />
</div>
) : (
<div className="flex items-center gap-3 w-full px-3 py-2 rounded-md border border-dashed border-border/50 bg-background/50">
<MessageSquare className="h-4 w-4 text-muted-foreground" />
<span className="text-xs text-muted-foreground font-normal">No conversations yet</span>
</div>
)}
</div>
) : (
chats.map((chat) => (
<SidebarMenuItem key={chat.id}>
<SidebarMenuButton
asChild
tooltip={isCollapsed ? chat.title : undefined}
data-active={pathname === `/chat/${chat.id}`}
className={cn(
"transition-all hover:bg-primary/10 active:bg-primary/15",
pathname === `/chat/${chat.id}` ? "bg-secondary/60 hover:bg-secondary/60" : ""
)}
>
<Link
href={`/chat/${chat.id}`}
className="flex items-center justify-between w-full gap-1"
>
<div className="flex items-center min-w-0 overflow-hidden flex-1 pr-2">
<MessageSquare className={cn(
"h-4 w-4 flex-shrink-0",
pathname === `/chat/${chat.id}` ? "text-foreground" : "text-muted-foreground"
)} />
{!isCollapsed && (
<span className={cn(
"ml-2 truncate text-sm",
pathname === `/chat/${chat.id}` ? "text-foreground font-medium" : "text-foreground/80"
)} title={chat.title}>
{chat.title.length > 18 ? `${chat.title.slice(0, 18)}...` : chat.title}
</span>
)}
</div>
{!isCollapsed && (
<Button
variant="ghost"
size="icon"
className="h-6 w-6 text-muted-foreground hover:text-foreground flex-shrink-0"
onClick={(e) => handleDeleteChat(chat.id, e)}
title="Delete chat"
>
<Trash2 className="h-3.5 w-3.5" />
</Button>
)}
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
))
)}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
<div className="relative my-0">
<div className="absolute inset-x-0">
<Separator className="w-full h-px bg-border/40" />
</div>
</div>
<SidebarGroup className="flex-shrink-0">
<SidebarGroupLabel className={cn(
"px-4 pt-0 text-xs font-medium text-muted-foreground/80 uppercase tracking-wider",
isCollapsed ? "sr-only" : ""
)}>
MCP Servers
</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton
onClick={() => setMcpSettingsOpen(true)}
className={cn(
"w-full flex items-center gap-2 transition-all",
"hover:bg-secondary/50 active:bg-secondary/70"
)}
tooltip={isCollapsed ? "MCP Servers" : undefined}
>
<ServerIcon className={cn(
"h-4 w-4 flex-shrink-0",
activeServersCount > 0 ? "text-primary" : "text-muted-foreground"
)} />
{!isCollapsed && (
<span className="flex-grow text-sm text-foreground/80">MCP Servers</span>
)}
{activeServersCount > 0 && !isCollapsed ? (
<Badge
variant="secondary"
className="ml-auto text-[10px] px-1.5 py-0 h-5 bg-secondary/80"
>
{activeServersCount}
</Badge>
) : activeServersCount > 0 && isCollapsed ? (
<SidebarMenuBadge className="bg-secondary/80 text-secondary-foreground">
{activeServersCount}
</SidebarMenuBadge>
) : null}
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
<SidebarFooter className="p-4 border-t border-border/40 mt-auto">
<div className={`flex flex-col ${isCollapsed ? "items-center" : ""} gap-3`}>
<Button
variant="default"
className={cn(
"w-full bg-primary text-primary-foreground hover:bg-primary/90",
isCollapsed ? "w-8 h-8 p-0" : ""
)}
onClick={handleNewChat}
title={isCollapsed ? "New Chat" : undefined}
>
<PlusCircle className={`${isCollapsed ? "" : "mr-2"} h-4 w-4`} />
{!isCollapsed && <span>New Chat</span>}
</Button>
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild>
{isCollapsed ? (
<Button
variant="ghost"
className="w-8 h-8 p-0 flex items-center justify-center"
>
<Avatar className="h-6 w-6 rounded-lg bg-secondary/60">
<AvatarFallback className="rounded-lg text-xs font-medium text-secondary-foreground">
{userId.substring(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
</Button>
) : (
<Button
variant="outline"
className="w-full justify-between font-normal bg-transparent border border-border/60 shadow-none px-2 h-10 hover:bg-secondary/50"
>
<div className="flex items-center gap-2">
<Avatar className="h-7 w-7 rounded-lg bg-secondary/60">
<AvatarFallback className="rounded-lg text-sm font-medium text-secondary-foreground">
{userId.substring(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
<div className="grid text-left text-sm leading-tight">
<span className="truncate font-medium text-foreground/90">User ID</span>
<span className="truncate text-xs text-muted-foreground">{userId.substring(0, 16)}...</span>
</div>
</div>
<ChevronsUpDown className="h-4 w-4 text-muted-foreground" />
</Button>
)}
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-56 rounded-lg"
side={isCollapsed ? "top" : "top"}
align={isCollapsed ? "start" : "end"}
sideOffset={8}
>
<DropdownMenuLabel className="p-0 font-normal">
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<Avatar className="h-8 w-8 rounded-lg bg-secondary/60">
<AvatarFallback className="rounded-lg text-sm font-medium text-secondary-foreground">
{userId.substring(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold text-foreground/90">User ID</span>
<span className="truncate text-xs text-muted-foreground">{userId}</span>
</div>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem onSelect={(e) => {
e.preventDefault();
navigator.clipboard.writeText(userId);
toast.success("User ID copied to clipboard");
}}>
<Copy className="mr-2 h-4 w-4 hover:text-sidebar-accent" />
Copy User ID
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem onSelect={(e) => {
e.preventDefault();
setMcpSettingsOpen(true);
}}>
<Settings className="mr-2 h-4 w-4 hover:text-sidebar-accent" />
MCP Settings
</DropdownMenuItem>
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
<div className="flex items-center justify-between w-full">
<div className="flex items-center">
<Sparkles className="mr-2 h-4 w-4 hover:text-sidebar-accent" />
Theme
</div>
<ThemeToggle className="h-6 w-6" />
</div>
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
<MCPServerManager
servers={mcpServers}
onServersChange={setMcpServers}
selectedServers={selectedMcpServers}
onSelectedServersChange={setSelectedMcpServers}
open={mcpSettingsOpen}
onOpenChange={setMcpSettingsOpen}
/>
</SidebarFooter>
</Sidebar>
);
}