"use client";
import { CreditCard, SquarePen, Zap } from "lucide-react";
import Link from "next/link";
import { useParams } from "next/navigation";
import { useTranslations } from "next-intl";
import { useMemo, useState } from "react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Progress } from "@/components/ui/progress";
import { Skeleton } from "@/components/ui/skeleton";
import { useIsAnonymous } from "@/contexts/anonymous-mode";
import { cn } from "@/lib/utils";
import { SIDEBAR_MIN_WIDTH } from "../../hooks/useSidebarResize";
import type { ChatItem, NavItem, PageUsage, SearchSpace, User } from "../../types/layout.types";
import { AuthenticatedPageUsageDisplay } from "./AuthenticatedPageUsageDisplay";
import { ChatListItem } from "./ChatListItem";
import { NavSection } from "./NavSection";
import { PremiumTokenUsageDisplay } from "./PremiumTokenUsageDisplay";
import { SidebarButton } from "./SidebarButton";
import { SidebarCollapseButton } from "./SidebarCollapseButton";
import { SidebarHeader } from "./SidebarHeader";
import { SidebarSection } from "./SidebarSection";
import { SidebarUserProfile } from "./SidebarUserProfile";
const CHAT_LIST_SKELETON_WIDTHS = ["w-[78%]", "w-[64%]", "w-[86%]", "w-[58%]", "w-[72%]"];
function ChatListItemSkeleton({ widthClass }: { widthClass: string }) {
return (
);
}
function ChatListSkeletonRows() {
return (
{CHAT_LIST_SKELETON_WIDTHS.map((widthClass) => (
))}
);
}
function CollapsedInboxIcon({ item }: { item: NavItem }) {
const Icon = item.icon;
return (
{typeof item.badge === "string" ? (
{item.badge}
) : null}
);
}
interface SidebarProps {
searchSpace: SearchSpace | null;
isCollapsed?: boolean;
onToggleCollapse?: () => void;
navItems: NavItem[];
onNavItemClick?: (item: NavItem) => void;
chats: ChatItem[];
sharedChats?: ChatItem[];
activeChatId?: number | null;
onNewChat: () => void;
onChatSelect: (chat: ChatItem) => void;
onChatRename?: (chat: ChatItem) => void;
onChatDelete?: (chat: ChatItem) => void;
onChatArchive?: (chat: ChatItem) => void;
onViewAllSharedChats?: () => void;
onViewAllPrivateChats?: () => void;
isSharedChatsPanelOpen?: boolean;
isPrivateChatsPanelOpen?: boolean;
user: User;
onSettings?: () => void;
onManageMembers?: () => void;
onUserSettings?: () => void;
onAnnouncements?: () => void;
announcementUnreadCount?: number;
onLogout?: () => void;
pageUsage?: PageUsage;
theme?: string;
setTheme?: (theme: "light" | "dark" | "system") => void;
className?: string;
isLoadingChats?: boolean;
disableTooltips?: boolean;
sidebarWidth?: number;
isResizing?: boolean;
renderUserProfile?: boolean;
}
export function Sidebar({
searchSpace,
isCollapsed = false,
onToggleCollapse,
navItems,
onNavItemClick,
chats,
sharedChats = [],
activeChatId,
onNewChat,
onChatSelect,
onChatRename,
onChatDelete,
onChatArchive,
onViewAllSharedChats,
onViewAllPrivateChats,
isSharedChatsPanelOpen = false,
isPrivateChatsPanelOpen = false,
user,
onSettings,
onManageMembers,
onUserSettings,
onAnnouncements,
announcementUnreadCount = 0,
onLogout,
pageUsage,
theme,
setTheme,
className,
isLoadingChats = false,
disableTooltips = false,
sidebarWidth = SIDEBAR_MIN_WIDTH,
isResizing = false,
renderUserProfile = true,
}: SidebarProps) {
const t = useTranslations("sidebar");
const [openDropdownChatId, setOpenDropdownChatId] = useState(null);
// Inbox and Documents are rendered explicitly right below New Chat. Pull
// them out of the nav items list so they don't also appear in the bottom
// NavSection. Documents is only present in navItems on mobile.
const inboxItem = useMemo(() => navItems.find((item) => item.url === "#inbox"), [navItems]);
const documentsItem = useMemo(
() => navItems.find((item) => item.url === "#documents"),
[navItems]
);
const footerNavItems = useMemo(
() => navItems.filter((item) => item.url !== "#inbox" && item.url !== "#documents"),
[navItems]
);
const collapsedWidth = 51;
return (
{})}
disableTooltip={disableTooltips}
/>
{inboxItem && (
onNavItemClick?.(inboxItem)}
isCollapsed={isCollapsed}
isActive={inboxItem.isActive}
badge={inboxItem.badge}
collapsedIconNode={}
tooltipContent={isCollapsed ? inboxItem.title : undefined}
buttonProps={
{
"data-joyride": "inbox-sidebar",
} as React.ButtonHTMLAttributes
}
/>
)}
{documentsItem && (
onNavItemClick?.(documentsItem)}
isCollapsed={isCollapsed}
isActive={documentsItem.isActive}
tooltipContent={isCollapsed ? documentsItem.title : undefined}
/>
)}
{/* Chat sections - fills available space */}
{isCollapsed ? (
) : (
{/* Shared Chats Section - takes only space needed, max 50% */}
{!disableTooltips && isSharedChatsPanelOpen ? t("hide") : t("show_all")}
) : undefined
}
>
{isLoadingChats ? (
) : sharedChats.length > 0 ? (
4 ? "pb-2" : ""}`}
>
{sharedChats.slice(0, 20).map((chat) => (
setOpenDropdownChatId(open ? chat.id : null)}
onClick={() => onChatSelect(chat)}
onRename={() => onChatRename?.(chat)}
onArchive={() => onChatArchive?.(chat)}
onDelete={() => onChatDelete?.(chat)}
/>
))}
{/* Gradient fade indicator when more than 4 items */}
{sharedChats.length > 4 && (
)}
) : (
{t("no_shared_chats")}
)}
{/* Private Chats Section - fills remaining space */}
{!disableTooltips && isPrivateChatsPanelOpen ? t("hide") : t("show_all")}
) : undefined
}
>
{isLoadingChats ? (
) : chats.length > 0 ? (
4 ? "pb-2" : ""}`}
>
{chats.slice(0, 20).map((chat) => (
setOpenDropdownChatId(open ? chat.id : null)}
onClick={() => onChatSelect(chat)}
onRename={() => onChatRename?.(chat)}
onArchive={() => onChatArchive?.(chat)}
onDelete={() => onChatDelete?.(chat)}
/>
))}
{/* Gradient fade indicator when more than 4 items */}
{chats.length > 4 && (
)}
) : (
{t("no_chats")}
)}
)}
{/* Footer */}
{/* Platform navigation */}
{footerNavItems.length > 0 && (
)}
0}
/>
{renderUserProfile && (
)}
);
}
function SidebarUsageFooter({
pageUsage,
isCollapsed,
hasNavSectionAbove = false,
}: {
pageUsage?: PageUsage;
isCollapsed: boolean;
hasNavSectionAbove?: boolean;
}) {
const params = useParams();
const searchSpaceId = params?.search_space_id ?? "";
const isAnonymous = useIsAnonymous();
if (isCollapsed) return null;
const containerClass = cn(
"px-3 py-3 space-y-3",
hasNavSectionAbove && "border-t"
);
if (isAnonymous) {
return (
{pageUsage && (
{pageUsage.pagesUsed.toLocaleString()} / {pageUsage.pagesLimit.toLocaleString()}{" "}
tokens
{Math.min(
(pageUsage.pagesUsed / Math.max(pageUsage.pagesLimit, 1)) * 100,
100
).toFixed(0)}
%
)}
Create Free Account
);
}
return (
Get Free Pages
FREE
Buy More
$1/1k · $1/1M
);
}