"use client"; import { AlertCircle, AtSign, BellDot, Check, CheckCheck, CheckCircle2, History, Inbox, ListFilter, Search, X, } from "lucide-react"; import { AnimatePresence, motion } from "motion/react"; import { useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; import { useCallback, useEffect, useMemo, useState } from "react"; import { createPortal } from "react-dom"; import { convertRenderedToDisplay } from "@/components/chat-comments/comment-item/comment-item"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Input } from "@/components/ui/input"; import { Spinner } from "@/components/ui/spinner"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { getConnectorIcon } from "@/contracts/enums/connectorIcons"; import type { InboxItem } from "@/hooks/use-inbox"; import type { ConnectorIndexingMetadata } from "@/contracts/types/inbox.types"; import { cn } from "@/lib/utils"; /** * Get initials from name or email for avatar fallback */ function getInitials(name: string | null | undefined, email: string | null | undefined): string { if (name) { return name .split(" ") .map((n) => n[0]) .join("") .toUpperCase() .slice(0, 2); } if (email) { const localPart = email.split("@")[0]; return localPart.slice(0, 2).toUpperCase(); } return "U"; } type InboxTab = "mentions" | "status"; type InboxFilter = "all" | "unread"; interface InboxSidebarProps { open: boolean; onOpenChange: (open: boolean) => void; inboxItems: InboxItem[]; unreadCount: number; loading: boolean; markAsRead: (id: number) => Promise; markAllAsRead: () => Promise; onCloseMobileSidebar?: () => void; } export function InboxSidebar({ open, onOpenChange, inboxItems, unreadCount, loading, markAsRead, markAllAsRead, onCloseMobileSidebar, }: InboxSidebarProps) { const t = useTranslations("sidebar"); const router = useRouter(); const [searchQuery, setSearchQuery] = useState(""); const [activeTab, setActiveTab] = useState("mentions"); const [activeFilter, setActiveFilter] = useState("all"); const [selectedConnector, setSelectedConnector] = useState(null); const [mounted, setMounted] = useState(false); // Dropdown state for filter menu const [openDropdown, setOpenDropdown] = useState<"filter" | null>(null); const [markingAsReadId, setMarkingAsReadId] = useState(null); useEffect(() => { setMounted(true); }, []); useEffect(() => { const handleEscape = (e: KeyboardEvent) => { if (e.key === "Escape" && open) { onOpenChange(false); } }; document.addEventListener("keydown", handleEscape); return () => document.removeEventListener("keydown", handleEscape); }, [open, onOpenChange]); useEffect(() => { if (open) { document.body.style.overflow = "hidden"; } else { document.body.style.overflow = ""; } return () => { document.body.style.overflow = ""; }; }, [open]); // Reset connector filter when switching away from status tab useEffect(() => { if (activeTab !== "status") { setSelectedConnector(null); } }, [activeTab]); // Split items by type const mentionItems = useMemo( () => inboxItems.filter((item) => item.type === "new_mention"), [inboxItems] ); const statusItems = useMemo( () => inboxItems.filter( (item) => item.type === "connector_indexing" || item.type === "document_processing" ), [inboxItems] ); // Get unique connectors from status items for filtering const uniqueConnectors = useMemo(() => { const connectorMap = new Map(); statusItems .filter((item) => item.type === "connector_indexing") .forEach((item) => { const metadata = item.metadata as ConnectorIndexingMetadata; if (metadata?.connector_type && !connectorMap.has(metadata.connector_type)) { connectorMap.set(metadata.connector_type, { type: metadata.connector_type, name: metadata.connector_name || metadata.connector_type, }); } }); return Array.from(connectorMap.values()); }, [statusItems]); // Get items for current tab const currentTabItems = activeTab === "mentions" ? mentionItems : statusItems; // Filter items based on filter type, connector filter, and search query const filteredItems = useMemo(() => { let items = currentTabItems; // Apply read/unread filter if (activeFilter === "unread") { items = items.filter((item) => !item.read); } // Apply connector filter (only for status tab) if (activeTab === "status" && selectedConnector) { items = items.filter((item) => { if (item.type === "connector_indexing") { const metadata = item.metadata as ConnectorIndexingMetadata; return metadata?.connector_type === selectedConnector; } return false; // Hide document_processing when a specific connector is selected }); } // Apply search query if (searchQuery.trim()) { const query = searchQuery.toLowerCase(); items = items.filter( (item) => item.title.toLowerCase().includes(query) || item.message.toLowerCase().includes(query) ); } return items; }, [currentTabItems, activeFilter, activeTab, selectedConnector, searchQuery]); // Count unread items per tab const unreadMentionsCount = useMemo(() => { return mentionItems.filter((item) => !item.read).length; }, [mentionItems]); const unreadStatusCount = useMemo(() => { return statusItems.filter((item) => !item.read).length; }, [statusItems]); const handleItemClick = useCallback( async (item: InboxItem) => { if (!item.read) { setMarkingAsReadId(item.id); await markAsRead(item.id); setMarkingAsReadId(null); } if (item.type === "new_mention") { const metadata = item.metadata as { thread_id?: number; comment_id?: number; }; const searchSpaceId = item.search_space_id; const threadId = metadata?.thread_id; const commentId = metadata?.comment_id; if (searchSpaceId && threadId) { const url = commentId ? `/dashboard/${searchSpaceId}/new-chat/${threadId}?commentId=${commentId}` : `/dashboard/${searchSpaceId}/new-chat/${threadId}`; onOpenChange(false); onCloseMobileSidebar?.(); router.push(url); } } }, [markAsRead, router, onOpenChange, onCloseMobileSidebar] ); const handleMarkAllAsRead = useCallback(async () => { await markAllAsRead(); }, [markAllAsRead]); const handleClearSearch = useCallback(() => { setSearchQuery(""); }, []); const formatTime = (dateString: string) => { try { const date = new Date(dateString); const now = new Date(); const diffMs = now.getTime() - date.getTime(); const diffMins = Math.floor(diffMs / (1000 * 60)); const diffHours = Math.floor(diffMs / (1000 * 60 * 60)); const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); if (diffMins < 1) return "now"; if (diffMins < 60) return `${diffMins}m`; if (diffHours < 24) return `${diffHours}h`; if (diffDays < 7) return `${diffDays}d`; return `${Math.floor(diffDays / 7)}w`; } catch { return "now"; } }; const getStatusIcon = (item: InboxItem) => { // For mentions, show the author's avatar with initials fallback if (item.type === "new_mention") { const metadata = item.metadata as { author_name?: string; author_avatar_url?: string | null; author_email?: string; }; const authorName = metadata?.author_name; const avatarUrl = metadata?.author_avatar_url; const authorEmail = metadata?.author_email; return ( {avatarUrl && } {getInitials(authorName, authorEmail)} ); } // For status items (connector/document), show status icons const status = item.metadata?.status as string | undefined; switch (status) { case "in_progress": return (
); case "completed": return (
); case "failed": return (
); default: return (
); } }; const getEmptyStateMessage = () => { if (activeTab === "mentions") { return { title: t("no_mentions") || "No mentions", hint: t("no_mentions_hint") || "You'll see mentions from others here", }; } return { title: t("no_status_updates") || "No status updates", hint: t("no_status_updates_hint") || "Document and connector updates will appear here", }; }; if (!mounted) return null; return createPortal( {open && ( <> onOpenChange(false)} aria-hidden="true" />

{t("inbox") || "Inbox"}

setOpenDropdown(isOpen ? "filter" : null)} > {t("filter") || "Filter"} {t("filter") || "Filter"} setActiveFilter("all")} className="flex items-center justify-between" > {t("all") || "All"} {activeFilter === "all" && } setActiveFilter("unread")} className="flex items-center justify-between" > {t("unread") || "Unread"} {activeFilter === "unread" && } {t("mark_all_read") || "Mark all as read"}
setSearchQuery(e.target.value)} className="pl-9 pr-8 h-9" /> {searchQuery && ( )}
setActiveTab(value as InboxTab)} className="shrink-0 mx-4" > {t("mentions") || "Mentions"} {unreadMentionsCount} {t("status") || "Status"} {unreadStatusCount} {/* Connector filter chips - only show in status tab when there are connectors */} {activeTab === "status" && uniqueConnectors.length > 0 && (
{/* Left shadow indicator */}
{/* Right shadow indicator */}
{uniqueConnectors.map((connector) => ( {connector.name} ))}
)}
{loading ? (
) : filteredItems.length > 0 ? (
{filteredItems.map((item) => { const isMarkingAsRead = markingAsReadId === item.id; return (

{item.title}

{convertRenderedToDisplay(item.message)}

{/* Time and unread dot - fixed width to prevent content shift */}
{formatTime(item.created_at)} {!item.read && ( )}
); })}
) : searchQuery ? (

{t("no_results_found") || "No results found"}

{t("try_different_search") || "Try a different search term"}

) : (
{activeTab === "mentions" ? ( ) : ( )}

{getEmptyStateMessage().title}

{getEmptyStateMessage().hint}

)}
)} , document.body ); }