refactor(chat): consolidate chat handling by merging shared and private chat sections into a unified chat view, update related components and translations

This commit is contained in:
Anish Sarkar 2026-06-03 00:07:13 +05:30
parent 9daaf12658
commit c002f45c8e
15 changed files with 116 additions and 826 deletions

View file

@ -3,7 +3,7 @@
import { Inbox, LibraryBig } from "lucide-react"; import { Inbox, LibraryBig } from "lucide-react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { Fragment, useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import { useAnonymousMode } from "@/contexts/anonymous-mode"; import { useAnonymousMode } from "@/contexts/anonymous-mode";
import { useLoginGate } from "@/contexts/login-gate"; import { useLoginGate } from "@/contexts/login-gate";
import { useAnnouncements } from "@/hooks/use-announcements"; import { useAnnouncements } from "@/hooks/use-announcements";
@ -110,15 +110,13 @@ export function FreeLayoutDataProvider({ children }: FreeLayoutDataProviderProps
navItems={navItems} navItems={navItems}
onNavItemClick={handleNavItemClick} onNavItemClick={handleNavItemClick}
chats={[]} chats={[]}
sharedChats={[]}
activeChatId={null} activeChatId={null}
onNewChat={resetChat} onNewChat={resetChat}
onChatSelect={handleChatSelect} onChatSelect={handleChatSelect}
onChatRename={gatedAction("rename chats")} onChatRename={gatedAction("rename chats")}
onChatDelete={gatedAction("delete chats")} onChatDelete={gatedAction("delete chats")}
onChatArchive={gatedAction("archive chats")} onChatArchive={gatedAction("archive chats")}
onViewAllSharedChats={gatedAction("view shared chats")} onViewAllChats={gatedAction("view chat history")}
onViewAllPrivateChats={gatedAction("view chat history")}
user={{ user={{
email: "Guest", email: "Guest",
name: "Guest", name: "Guest",
@ -137,7 +135,7 @@ export function FreeLayoutDataProvider({ children }: FreeLayoutDataProviderProps
onOpenChange: setIsDocsSidebarOpen, onOpenChange: setIsDocsSidebarOpen,
}} }}
> >
<Fragment>{children}</Fragment> {children}
</LayoutShell> </LayoutShell>
); );
} }

View file

@ -121,7 +121,7 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
}); });
// Unified slide-out panel state (only one can be open at a time) // Unified slide-out panel state (only one can be open at a time)
type SlideoutPanel = "inbox" | "shared" | "private" | null; type SlideoutPanel = "inbox" | "chats" | null;
const [activeSlideoutPanel, setActiveSlideoutPanel] = useState<SlideoutPanel>(null); const [activeSlideoutPanel, setActiveSlideoutPanel] = useState<SlideoutPanel>(null);
const isInboxSidebarOpen = activeSlideoutPanel === "inbox"; const isInboxSidebarOpen = activeSlideoutPanel === "inbox";
@ -304,34 +304,17 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
}); });
}, [currentChatId, searchSpaceId, threadsData?.threads, syncChatTab]); }, [currentChatId, searchSpaceId, threadsData?.threads, syncChatTab]);
// Transform and split chats into private and shared based on visibility const chats = useMemo(() => {
const { myChats, sharedChats } = useMemo(() => { if (!threadsData?.threads) return [];
if (!threadsData?.threads) return { myChats: [], sharedChats: [] };
const privateChats: ChatItem[] = []; return threadsData.threads.map<ChatItem>((thread) => ({
const sharedChatsList: ChatItem[] = []; id: thread.id,
name: thread.title || `Chat ${thread.id}`,
for (const thread of threadsData.threads) { url: `/dashboard/${searchSpaceId}/new-chat/${thread.id}`,
const chatItem: ChatItem = { visibility: thread.visibility,
id: thread.id, isOwnThread: thread.is_own_thread,
name: thread.title || `Chat ${thread.id}`, archived: thread.archived,
url: `/dashboard/${searchSpaceId}/new-chat/${thread.id}`, }));
visibility: thread.visibility,
isOwnThread: thread.is_own_thread,
archived: thread.archived,
};
// Split based on visibility, not ownership:
// - PRIVATE chats go to "Private Chats" section
// - SEARCH_SPACE chats go to "Shared Chats" section
if (thread.visibility === "SEARCH_SPACE") {
sharedChatsList.push(chatItem);
} else {
privateChats.push(chatItem);
}
}
return { myChats: privateChats, sharedChats: sharedChatsList };
}, [threadsData, searchSpaceId]); }, [threadsData, searchSpaceId]);
// Navigation items // Navigation items
@ -599,12 +582,8 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
} }
}, [router]); }, [router]);
const handleViewAllSharedChats = useCallback(() => { const handleViewAllChats = useCallback(() => {
setActiveSlideoutPanel((prev) => (prev === "shared" ? null : "shared")); setActiveSlideoutPanel((prev) => (prev === "chats" ? null : "chats"));
}, []);
const handleViewAllPrivateChats = useCallback(() => {
setActiveSlideoutPanel((prev) => (prev === "private" ? null : "private"));
}, []); }, []);
// Delete handlers // Delete handlers
@ -695,16 +674,14 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
searchSpace={activeSearchSpace} searchSpace={activeSearchSpace}
navItems={navItems} navItems={navItems}
onNavItemClick={handleNavItemClick} onNavItemClick={handleNavItemClick}
chats={myChats} chats={chats}
sharedChats={sharedChats}
activeChatId={currentChatId} activeChatId={currentChatId}
onNewChat={handleNewChat} onNewChat={handleNewChat}
onChatSelect={handleChatSelect} onChatSelect={handleChatSelect}
onChatRename={handleChatRename} onChatRename={handleChatRename}
onChatDelete={handleChatDelete} onChatDelete={handleChatDelete}
onChatArchive={handleChatArchive} onChatArchive={handleChatArchive}
onViewAllSharedChats={handleViewAllSharedChats} onViewAllChats={handleViewAllChats}
onViewAllPrivateChats={handleViewAllPrivateChats}
user={{ user={{
email: user?.email || "", email: user?.email || "",
name: user?.display_name || user?.email?.split("@")[0], name: user?.display_name || user?.email?.split("@")[0],
@ -759,10 +736,7 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
markAllAsRead: statusInbox.markAllAsRead, markAllAsRead: statusInbox.markAllAsRead,
}, },
}} }}
allSharedChatsPanel={{ allChatsPanel={{
searchSpaceId,
}}
allPrivateChatsPanel={{
searchSpaceId, searchSpaceId,
}} }}
documentsPanel={{ documentsPanel={{

View file

@ -70,8 +70,7 @@ export interface ChatsSectionProps {
activeChatId?: number | null; activeChatId?: number | null;
onChatSelect: (chat: ChatItem) => void; onChatSelect: (chat: ChatItem) => void;
onChatDelete?: (chat: ChatItem) => void; onChatDelete?: (chat: ChatItem) => void;
onViewAllSharedChats?: () => void; onViewAllChats?: () => void;
onViewAllPrivateChats?: () => void;
searchSpaceId?: string; searchSpaceId?: string;
} }
@ -96,13 +95,11 @@ export interface SidebarProps {
searchSpaceId?: string; searchSpaceId?: string;
navItems: NavItem[]; navItems: NavItem[];
chats: ChatItem[]; chats: ChatItem[];
sharedChats?: ChatItem[];
activeChatId?: number | null; activeChatId?: number | null;
onNewChat: () => void; onNewChat: () => void;
onChatSelect: (chat: ChatItem) => void; onChatSelect: (chat: ChatItem) => void;
onChatDelete?: (chat: ChatItem) => void; onChatDelete?: (chat: ChatItem) => void;
onViewAllSharedChats?: () => void; onViewAllChats?: () => void;
onViewAllPrivateChats?: () => void;
user: User; user: User;
theme?: string; theme?: string;
onSettings?: () => void; onSettings?: () => void;

View file

@ -27,8 +27,7 @@ import {
RightPanelToggleButton, RightPanelToggleButton,
} from "../right-panel/RightPanel"; } from "../right-panel/RightPanel";
import { import {
AllPrivateChatsSidebarContent, AllChatsSidebarContent,
AllSharedChatsSidebarContent,
DocumentsSidebar, DocumentsSidebar,
InboxSidebarContent, InboxSidebarContent,
MobileSidebar, MobileSidebar,
@ -94,7 +93,7 @@ interface TabDataSource {
markAllAsRead: () => Promise<boolean>; markAllAsRead: () => Promise<boolean>;
} }
export type ActiveSlideoutPanel = "inbox" | "shared" | "private" | null; export type ActiveSlideoutPanel = "inbox" | "chats" | null;
// Inbox-related props — per-tab data sources with independent loading/pagination // Inbox-related props — per-tab data sources with independent loading/pagination
interface InboxProps { interface InboxProps {
@ -115,15 +114,13 @@ interface LayoutShellProps {
navItems: NavItem[]; navItems: NavItem[];
onNavItemClick?: (item: NavItem) => void; onNavItemClick?: (item: NavItem) => void;
chats: ChatItem[]; chats: ChatItem[];
sharedChats?: ChatItem[];
activeChatId?: number | null; activeChatId?: number | null;
onNewChat: () => void; onNewChat: () => void;
onChatSelect: (chat: ChatItem) => void; onChatSelect: (chat: ChatItem) => void;
onChatRename?: (chat: ChatItem) => void; onChatRename?: (chat: ChatItem) => void;
onChatDelete?: (chat: ChatItem) => void; onChatDelete?: (chat: ChatItem) => void;
onChatArchive?: (chat: ChatItem) => void; onChatArchive?: (chat: ChatItem) => void;
onViewAllSharedChats?: () => void; onViewAllChats?: () => void;
onViewAllPrivateChats?: () => void;
user: User; user: User;
onSettings?: () => void; onSettings?: () => void;
onManageMembers?: () => void; onManageMembers?: () => void;
@ -148,10 +145,7 @@ interface LayoutShellProps {
inbox?: InboxProps; inbox?: InboxProps;
isLoadingChats?: boolean; isLoadingChats?: boolean;
// All chats panel props // All chats panel props
allSharedChatsPanel?: { allChatsPanel?: {
searchSpaceId: string;
};
allPrivateChatsPanel?: {
searchSpaceId: string; searchSpaceId: string;
}; };
documentsPanel?: { documentsPanel?: {
@ -226,15 +220,13 @@ export function LayoutShell({
navItems, navItems,
onNavItemClick, onNavItemClick,
chats, chats,
sharedChats,
activeChatId, activeChatId,
onNewChat, onNewChat,
onChatSelect, onChatSelect,
onChatRename, onChatRename,
onChatDelete, onChatDelete,
onChatArchive, onChatArchive,
onViewAllSharedChats, onViewAllChats,
onViewAllPrivateChats,
user, user,
onSettings, onSettings,
onManageMembers, onManageMembers,
@ -256,8 +248,7 @@ export function LayoutShell({
onSlideoutPanelChange, onSlideoutPanelChange,
inbox, inbox,
isLoadingChats = false, isLoadingChats = false,
allSharedChatsPanel, allChatsPanel,
allPrivateChatsPanel,
documentsPanel, documentsPanel,
onTabSwitch, onTabSwitch,
}: LayoutShellProps) { }: LayoutShellProps) {
@ -288,13 +279,7 @@ export function LayoutShell({
const anySlideOutOpen = activeSlideoutPanel !== null; const anySlideOutOpen = activeSlideoutPanel !== null;
const panelAriaLabel = const panelAriaLabel =
activeSlideoutPanel === "inbox" activeSlideoutPanel === "inbox" ? "Inbox" : activeSlideoutPanel === "chats" ? "Chats" : "Panel";
? "Inbox"
: activeSlideoutPanel === "shared"
? "Shared Chats"
: activeSlideoutPanel === "private"
? "Private Chats"
: "Panel";
// Mobile layout // Mobile layout
if (isMobile) { if (isMobile) {
@ -317,17 +302,14 @@ export function LayoutShell({
navItems={navItems} navItems={navItems}
onNavItemClick={onNavItemClick} onNavItemClick={onNavItemClick}
chats={chats} chats={chats}
sharedChats={sharedChats}
activeChatId={activeChatId} activeChatId={activeChatId}
onNewChat={onNewChat} onNewChat={onNewChat}
onChatSelect={onChatSelect} onChatSelect={onChatSelect}
onChatRename={onChatRename} onChatRename={onChatRename}
onChatDelete={onChatDelete} onChatDelete={onChatDelete}
onChatArchive={onChatArchive} onChatArchive={onChatArchive}
onViewAllSharedChats={onViewAllSharedChats} onViewAllChats={onViewAllChats}
onViewAllPrivateChats={onViewAllPrivateChats} isChatsPanelOpen={activeSlideoutPanel === "chats"}
isSharedChatsPanelOpen={activeSlideoutPanel === "shared"}
isPrivateChatsPanelOpen={activeSlideoutPanel === "private"}
user={user} user={user}
onSettings={onSettings} onSettings={onSettings}
onManageMembers={onManageMembers} onManageMembers={onManageMembers}
@ -379,34 +361,18 @@ export function LayoutShell({
/> />
</motion.div> </motion.div>
)} )}
{activeSlideoutPanel === "shared" && allSharedChatsPanel && ( {activeSlideoutPanel === "chats" && allChatsPanel && (
<motion.div <motion.div
key="shared" key="chats"
className="h-full flex flex-col" className="h-full flex flex-col"
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
exit={{ opacity: 0 }} exit={{ opacity: 0 }}
transition={{ duration: 0.15 }} transition={{ duration: 0.15 }}
> >
<AllSharedChatsSidebarContent <AllChatsSidebarContent
onOpenChange={(open) => closeSlideout(open)} onOpenChange={(open) => closeSlideout(open)}
searchSpaceId={allSharedChatsPanel.searchSpaceId} searchSpaceId={allChatsPanel.searchSpaceId}
onCloseMobileSidebar={() => setMobileMenuOpen(false)}
/>
</motion.div>
)}
{activeSlideoutPanel === "private" && allPrivateChatsPanel && (
<motion.div
key="private"
className="h-full flex flex-col"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.15 }}
>
<AllPrivateChatsSidebarContent
onOpenChange={(open) => closeSlideout(open)}
searchSpaceId={allPrivateChatsPanel.searchSpaceId}
onCloseMobileSidebar={() => setMobileMenuOpen(false)} onCloseMobileSidebar={() => setMobileMenuOpen(false)}
/> />
</motion.div> </motion.div>
@ -478,17 +444,14 @@ export function LayoutShell({
navItems={navItems} navItems={navItems}
onNavItemClick={onNavItemClick} onNavItemClick={onNavItemClick}
chats={chats} chats={chats}
sharedChats={sharedChats}
activeChatId={activeChatId} activeChatId={activeChatId}
onNewChat={onNewChat} onNewChat={onNewChat}
onChatSelect={onChatSelect} onChatSelect={onChatSelect}
onChatRename={onChatRename} onChatRename={onChatRename}
onChatDelete={onChatDelete} onChatDelete={onChatDelete}
onChatArchive={onChatArchive} onChatArchive={onChatArchive}
onViewAllSharedChats={onViewAllSharedChats} onViewAllChats={onViewAllChats}
onViewAllPrivateChats={onViewAllPrivateChats} isChatsPanelOpen={activeSlideoutPanel === "chats"}
isSharedChatsPanelOpen={activeSlideoutPanel === "shared"}
isPrivateChatsPanelOpen={activeSlideoutPanel === "private"}
user={user} user={user}
onSettings={onSettings} onSettings={onSettings}
onManageMembers={onManageMembers} onManageMembers={onManageMembers}
@ -554,33 +517,18 @@ export function LayoutShell({
/> />
</motion.div> </motion.div>
)} )}
{activeSlideoutPanel === "shared" && allSharedChatsPanel && ( {activeSlideoutPanel === "chats" && allChatsPanel && (
<motion.div <motion.div
key="shared" key="chats"
className="h-full flex flex-col" className="h-full flex flex-col"
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
exit={{ opacity: 0 }} exit={{ opacity: 0 }}
transition={{ duration: 0.15 }} transition={{ duration: 0.15 }}
> >
<AllSharedChatsSidebarContent <AllChatsSidebarContent
onOpenChange={(open) => closeSlideout(open)} onOpenChange={(open) => closeSlideout(open)}
searchSpaceId={allSharedChatsPanel.searchSpaceId} searchSpaceId={allChatsPanel.searchSpaceId}
/>
</motion.div>
)}
{activeSlideoutPanel === "private" && allPrivateChatsPanel && (
<motion.div
key="private"
className="h-full flex flex-col"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.15 }}
>
<AllPrivateChatsSidebarContent
onOpenChange={(open) => closeSlideout(open)}
searchSpaceId={allPrivateChatsPanel.searchSpaceId}
/> />
</motion.div> </motion.div>
)} )}

View file

@ -12,6 +12,7 @@ import {
Search, Search,
Trash2, Trash2,
User, User,
Users,
X, X,
} from "lucide-react"; } from "lucide-react";
import { useParams, useRouter } from "next/navigation"; import { useParams, useRouter } from "next/navigation";
@ -52,21 +53,21 @@ import { formatThreadTimestamp } from "@/lib/format-date";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { SidebarSlideOutPanel } from "./SidebarSlideOutPanel"; import { SidebarSlideOutPanel } from "./SidebarSlideOutPanel";
export interface AllPrivateChatsSidebarContentProps { export interface AllChatsSidebarContentProps {
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
searchSpaceId: string; searchSpaceId: string;
onCloseMobileSidebar?: () => void; onCloseMobileSidebar?: () => void;
} }
interface AllPrivateChatsSidebarProps extends AllPrivateChatsSidebarContentProps { interface AllChatsSidebarProps extends AllChatsSidebarContentProps {
open: boolean; open: boolean;
} }
export function AllPrivateChatsSidebarContent({ export function AllChatsSidebarContent({
onOpenChange, onOpenChange,
searchSpaceId, searchSpaceId,
onCloseMobileSidebar, onCloseMobileSidebar,
}: AllPrivateChatsSidebarContentProps) { }: AllChatsSidebarContentProps) {
const t = useTranslations("sidebar"); const t = useTranslations("sidebar");
const router = useRouter(); const router = useRouter();
const params = useParams(); const params = useParams();
@ -122,28 +123,20 @@ export function AllPrivateChatsSidebarContent({
enabled: !!searchSpaceId && isSearchMode, enabled: !!searchSpaceId && isSearchMode,
}); });
// Filter to only private chats (PRIVATE visibility or no visibility set)
const { activeChats, archivedChats } = useMemo(() => { const { activeChats, archivedChats } = useMemo(() => {
if (isSearchMode) { if (isSearchMode) {
const privateSearchResults = (searchData ?? []).filter(
(thread) => thread.visibility !== "SEARCH_SPACE"
);
return { return {
activeChats: privateSearchResults.filter((t) => !t.archived), activeChats: (searchData ?? []).filter((t) => !t.archived),
archivedChats: privateSearchResults.filter((t) => t.archived), archivedChats: (searchData ?? []).filter((t) => t.archived),
}; };
} }
if (!threadsData) return { activeChats: [], archivedChats: [] }; if (!threadsData) return { activeChats: [], archivedChats: [] };
const activePrivate = threadsData.threads.filter( return {
(thread) => thread.visibility !== "SEARCH_SPACE" activeChats: threadsData.threads,
); archivedChats: threadsData.archived_threads,
const archivedPrivate = threadsData.archived_threads.filter( };
(thread) => thread.visibility !== "SEARCH_SPACE"
);
return { activeChats: activePrivate, archivedChats: archivedPrivate };
}, [threadsData, searchData, isSearchMode]); }, [threadsData, searchData, isSearchMode]);
const threads = showArchived ? archivedChats : activeChats; const threads = showArchived ? archivedChats : activeChats;
@ -265,7 +258,7 @@ export function AllPrivateChatsSidebarContent({
<span className="sr-only">{t("close") || "Close"}</span> <span className="sr-only">{t("close") || "Close"}</span>
</Button> </Button>
)} )}
<h2 className="text-lg font-semibold">{t("chats") || "Private Chats"}</h2> <h2 className="text-lg font-semibold">{t("chats") || "Chats"}</h2>
</div> </div>
<div className="relative"> <div className="relative">
@ -370,7 +363,13 @@ export function AllPrivateChatsSidebarContent({
isBusy && "opacity-50 pointer-events-none" isBusy && "opacity-50 pointer-events-none"
)} )}
> >
<span className="truncate">{thread.title || "New Chat"}</span> <span className="min-w-0 flex-1 truncate">{thread.title || "New Chat"}</span>
{thread.visibility === "SEARCH_SPACE" ? (
<Users
aria-label={t("shared_chat") || "Shared chat"}
className="h-3 w-3 shrink-0 text-muted-foreground/50"
/>
) : null}
</Button> </Button>
) : ( ) : (
<Tooltip delayDuration={600}> <Tooltip delayDuration={600}>
@ -388,7 +387,15 @@ export function AllPrivateChatsSidebarContent({
isBusy && "opacity-50 pointer-events-none" isBusy && "opacity-50 pointer-events-none"
)} )}
> >
<span className="truncate">{thread.title || "New Chat"}</span> <span className="min-w-0 flex-1 truncate">
{thread.title || "New Chat"}
</span>
{thread.visibility === "SEARCH_SPACE" ? (
<Users
aria-label={t("shared_chat") || "Shared chat"}
className="h-3 w-3 shrink-0 text-muted-foreground/50"
/>
) : null}
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="bottom" align="start"> <TooltipContent side="bottom" align="start">
@ -486,7 +493,7 @@ export function AllPrivateChatsSidebarContent({
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
{showArchived {showArchived
? t("no_archived_chats") || "No archived chats" ? t("no_archived_chats") || "No archived chats"
: t("no_chats") || "No private chats"} : t("no_chats") || "No chats"}
</p> </p>
{!showArchived && ( {!showArchived && (
<p className="mt-1 text-[11px] text-muted-foreground/70"> <p className="mt-1 text-[11px] text-muted-foreground/70">
@ -545,21 +552,17 @@ export function AllPrivateChatsSidebarContent({
); );
} }
export function AllPrivateChatsSidebar({ export function AllChatsSidebar({
open, open,
onOpenChange, onOpenChange,
searchSpaceId, searchSpaceId,
onCloseMobileSidebar, onCloseMobileSidebar,
}: AllPrivateChatsSidebarProps) { }: AllChatsSidebarProps) {
const t = useTranslations("sidebar"); const t = useTranslations("sidebar");
return ( return (
<SidebarSlideOutPanel <SidebarSlideOutPanel open={open} onOpenChange={onOpenChange} ariaLabel={t("chats") || "Chats"}>
open={open} <AllChatsSidebarContent
onOpenChange={onOpenChange}
ariaLabel={t("chats") || "Private Chats"}
>
<AllPrivateChatsSidebarContent
onOpenChange={onOpenChange} onOpenChange={onOpenChange}
searchSpaceId={searchSpaceId} searchSpaceId={searchSpaceId}
onCloseMobileSidebar={onCloseMobileSidebar} onCloseMobileSidebar={onCloseMobileSidebar}

View file

@ -1,568 +0,0 @@
"use client";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useSetAtom } from "jotai";
import {
ArchiveIcon,
ChevronLeft,
MessageCircleMore,
MoreHorizontal,
Pencil,
RotateCcwIcon,
Search,
Trash2,
Users,
X,
} from "lucide-react";
import { useParams, useRouter } from "next/navigation";
import { useTranslations } from "next-intl";
import { useCallback, useMemo, useRef, useState } from "react";
import { toast } from "sonner";
import { removeChatTabAtom } from "@/atoms/tabs/tabs.atom";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/animated-tabs";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import { Skeleton } from "@/components/ui/skeleton";
import { Spinner } from "@/components/ui/spinner";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { useDebouncedValue } from "@/hooks/use-debounced-value";
import { useLongPress } from "@/hooks/use-long-press";
import { useIsMobile } from "@/hooks/use-mobile";
import {
deleteThread,
fetchThreads,
searchThreads,
updateThread,
} from "@/lib/chat/thread-persistence";
import { formatThreadTimestamp } from "@/lib/format-date";
import { cn } from "@/lib/utils";
import { SidebarSlideOutPanel } from "./SidebarSlideOutPanel";
export interface AllSharedChatsSidebarContentProps {
onOpenChange: (open: boolean) => void;
searchSpaceId: string;
onCloseMobileSidebar?: () => void;
}
interface AllSharedChatsSidebarProps extends AllSharedChatsSidebarContentProps {
open: boolean;
}
export function AllSharedChatsSidebarContent({
onOpenChange,
searchSpaceId,
onCloseMobileSidebar,
}: AllSharedChatsSidebarContentProps) {
const t = useTranslations("sidebar");
const router = useRouter();
const params = useParams();
const queryClient = useQueryClient();
const isMobile = useIsMobile();
const removeChatTab = useSetAtom(removeChatTabAtom);
const currentChatId = Array.isArray(params.chat_id)
? Number(params.chat_id[0])
: params.chat_id
? Number(params.chat_id)
: null;
const [deletingThreadId, setDeletingThreadId] = useState<number | null>(null);
const [archivingThreadId, setArchivingThreadId] = useState<number | null>(null);
const [searchQuery, setSearchQuery] = useState("");
const [showArchived, setShowArchived] = useState(false);
const [openDropdownId, setOpenDropdownId] = useState<number | null>(null);
const [showRenameDialog, setShowRenameDialog] = useState(false);
const [renamingThread, setRenamingThread] = useState<{ id: number; title: string } | null>(null);
const [newTitle, setNewTitle] = useState("");
const [isRenaming, setIsRenaming] = useState(false);
const debouncedSearchQuery = useDebouncedValue(searchQuery, 300);
const pendingThreadIdRef = useRef<number | null>(null);
const { handlers: longPressHandlers, wasLongPress } = useLongPress(
useCallback(() => {
if (pendingThreadIdRef.current !== null) {
setOpenDropdownId(pendingThreadIdRef.current);
}
}, [])
);
const isSearchMode = !!debouncedSearchQuery.trim();
const {
data: threadsData,
error: threadsError,
isLoading: isLoadingThreads,
} = useQuery({
queryKey: ["all-threads", searchSpaceId],
queryFn: () => fetchThreads(Number(searchSpaceId)),
enabled: !!searchSpaceId && !isSearchMode,
});
const {
data: searchData,
error: searchError,
isLoading: isLoadingSearch,
} = useQuery({
queryKey: ["search-threads", searchSpaceId, debouncedSearchQuery],
queryFn: () => searchThreads(Number(searchSpaceId), debouncedSearchQuery.trim()),
enabled: !!searchSpaceId && isSearchMode,
});
// Filter to only shared chats (SEARCH_SPACE visibility)
const { activeChats, archivedChats } = useMemo(() => {
if (isSearchMode) {
const sharedSearchResults = (searchData ?? []).filter(
(thread) => thread.visibility === "SEARCH_SPACE"
);
return {
activeChats: sharedSearchResults.filter((t) => !t.archived),
archivedChats: sharedSearchResults.filter((t) => t.archived),
};
}
if (!threadsData) return { activeChats: [], archivedChats: [] };
const activeShared = threadsData.threads.filter(
(thread) => thread.visibility === "SEARCH_SPACE"
);
const archivedShared = threadsData.archived_threads.filter(
(thread) => thread.visibility === "SEARCH_SPACE"
);
return { activeChats: activeShared, archivedChats: archivedShared };
}, [threadsData, searchData, isSearchMode]);
const threads = showArchived ? archivedChats : activeChats;
const handleThreadClick = useCallback(
(threadId: number) => {
router.push(`/dashboard/${searchSpaceId}/new-chat/${threadId}`);
onOpenChange(false);
onCloseMobileSidebar?.();
},
[router, onOpenChange, searchSpaceId, onCloseMobileSidebar]
);
const handleDeleteThread = useCallback(
async (threadId: number) => {
setDeletingThreadId(threadId);
try {
await deleteThread(threadId);
const fallbackTab = removeChatTab(threadId);
toast.success(t("chat_deleted") || "Chat deleted successfully");
queryClient.invalidateQueries({ queryKey: ["all-threads", searchSpaceId] });
queryClient.invalidateQueries({ queryKey: ["search-threads", searchSpaceId] });
queryClient.invalidateQueries({ queryKey: ["threads", searchSpaceId] });
if (currentChatId === threadId) {
onOpenChange(false);
setTimeout(() => {
if (fallbackTab?.type === "chat" && fallbackTab.chatUrl) {
router.push(fallbackTab.chatUrl);
return;
}
router.push(`/dashboard/${searchSpaceId}/new-chat`);
}, 250);
}
} catch (error) {
console.error("Error deleting thread:", error);
toast.error(t("error_deleting_chat") || "Failed to delete chat");
} finally {
setDeletingThreadId(null);
}
},
[queryClient, searchSpaceId, t, currentChatId, router, onOpenChange, removeChatTab]
);
const handleToggleArchive = useCallback(
async (threadId: number, currentlyArchived: boolean) => {
setArchivingThreadId(threadId);
try {
await updateThread(threadId, { archived: !currentlyArchived });
toast.success(
currentlyArchived
? t("chat_unarchived") || "Chat restored"
: t("chat_archived") || "Chat archived"
);
queryClient.invalidateQueries({ queryKey: ["all-threads", searchSpaceId] });
queryClient.invalidateQueries({ queryKey: ["search-threads", searchSpaceId] });
queryClient.invalidateQueries({ queryKey: ["threads", searchSpaceId] });
} catch (error) {
console.error("Error archiving thread:", error);
toast.error(t("error_archiving_chat") || "Failed to archive chat");
} finally {
setArchivingThreadId(null);
}
},
[queryClient, searchSpaceId, t]
);
const handleStartRename = useCallback((threadId: number, title: string) => {
setRenamingThread({ id: threadId, title });
setNewTitle(title);
setShowRenameDialog(true);
}, []);
const handleConfirmRename = useCallback(async () => {
if (!renamingThread || !newTitle.trim()) return;
setIsRenaming(true);
try {
await updateThread(renamingThread.id, { title: newTitle.trim() });
toast.success(t("chat_renamed") || "Chat renamed");
queryClient.invalidateQueries({ queryKey: ["all-threads", searchSpaceId] });
queryClient.invalidateQueries({ queryKey: ["search-threads", searchSpaceId] });
queryClient.invalidateQueries({ queryKey: ["threads", searchSpaceId] });
queryClient.invalidateQueries({
queryKey: ["threads", searchSpaceId, "detail", String(renamingThread.id)],
});
} catch (error) {
console.error("Error renaming thread:", error);
toast.error(t("error_renaming_chat") || "Failed to rename chat");
} finally {
setIsRenaming(false);
setShowRenameDialog(false);
setRenamingThread(null);
setNewTitle("");
}
}, [renamingThread, newTitle, queryClient, searchSpaceId, t]);
const handleClearSearch = useCallback(() => {
setSearchQuery("");
}, []);
const isLoading = isSearchMode ? isLoadingSearch : isLoadingThreads;
const error = isSearchMode ? searchError : threadsError;
const activeCount = activeChats.length;
const archivedCount = archivedChats.length;
return (
<>
<div className="shrink-0 p-3 pb-1.5 space-y-2">
<div className="flex items-center gap-2">
{isMobile && (
<Button
variant="ghost"
size="icon"
className="h-8 w-8 rounded-full text-muted-foreground hover:text-accent-foreground"
onClick={() => onOpenChange(false)}
>
<ChevronLeft className="h-4 w-4" />
<span className="sr-only">{t("close") || "Close"}</span>
</Button>
)}
<h2 className="text-lg font-semibold">{t("shared_chats") || "Shared Chats"}</h2>
</div>
<div className="relative">
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
<Input
type="text"
placeholder={t("search_chats") || "Search chats..."}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="h-8 border-0 bg-muted pl-8 pr-7 text-sm shadow-none"
/>
{searchQuery && (
<Button
variant="ghost"
size="icon"
className="absolute right-1 top-1/2 h-5 w-5 -translate-y-1/2 rounded-sm text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground"
onClick={handleClearSearch}
>
<X className="h-3.5 w-3.5" />
<span className="sr-only">{t("clear_search") || "Clear search"}</span>
</Button>
)}
</div>
</div>
{!isSearchMode && (
<Tabs
value={showArchived ? "archived" : "active"}
onValueChange={(value) => setShowArchived(value === "archived")}
className="shrink-0 mx-3 mt-1.5"
>
<TabsList stretch showBottomBorder size="sm">
<TabsTrigger value="active">
<span className="inline-flex items-center gap-1.5">
<MessageCircleMore className="h-3.5 w-3.5" />
<span>Active</span>
<span className="inline-flex h-4.5 min-w-4.5 items-center justify-center rounded-full bg-primary/20 px-1 text-[10px] font-medium text-muted-foreground">
{activeCount}
</span>
</span>
</TabsTrigger>
<TabsTrigger value="archived">
<span className="inline-flex items-center gap-1.5">
<ArchiveIcon className="h-3.5 w-3.5" />
<span>Archived</span>
<span className="inline-flex h-4.5 min-w-4.5 items-center justify-center rounded-full bg-primary/20 px-1 text-[10px] font-medium text-muted-foreground">
{archivedCount}
</span>
</span>
</TabsTrigger>
</TabsList>
</Tabs>
)}
<div className="flex-1 overflow-y-auto overflow-x-hidden p-1.5">
{isLoading ? (
<div className="space-y-1">
{[75, 90, 55, 80, 65, 85].map((titleWidth) => (
<div
key={`skeleton-${titleWidth}`}
className="flex items-center gap-2 rounded-md px-2 py-1.5"
>
<Skeleton className="h-4 w-4 shrink-0 rounded" />
<Skeleton className="h-4 rounded" style={{ width: `${titleWidth}%` }} />
</div>
))}
</div>
) : error ? (
<div className="text-center py-8 text-sm text-destructive">
{t("error_loading_chats") || "Error loading chats"}
</div>
) : threads.length > 0 ? (
<div className="space-y-1">
{threads.map((thread) => {
const isDeleting = deletingThreadId === thread.id;
const isArchiving = archivingThreadId === thread.id;
const isBusy = isDeleting || isArchiving;
const isActive = currentChatId === thread.id;
return (
<div key={thread.id} className="group/item relative w-full">
{isMobile ? (
<Button
type="button"
variant="ghost"
onClick={() => {
if (wasLongPress()) return;
handleThreadClick(thread.id);
}}
onTouchStart={() => {
pendingThreadIdRef.current = thread.id;
longPressHandlers.onTouchStart();
}}
onTouchEnd={longPressHandlers.onTouchEnd}
onTouchMove={longPressHandlers.onTouchMove}
disabled={isBusy}
className={cn(
"h-auto w-full justify-start gap-2 overflow-hidden rounded-md px-2 py-1.5 text-sm text-left",
"group-hover/item:bg-accent group-hover/item:text-accent-foreground",
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
isActive && "bg-accent text-accent-foreground",
isBusy && "opacity-50 pointer-events-none"
)}
>
<span className="truncate">{thread.title || "New Chat"}</span>
</Button>
) : (
<Tooltip delayDuration={600}>
<TooltipTrigger asChild>
<Button
type="button"
variant="ghost"
onClick={() => handleThreadClick(thread.id)}
disabled={isBusy}
className={cn(
"h-auto w-full justify-start gap-2 overflow-hidden rounded-md px-2 py-1.5 text-sm text-left",
"group-hover/item:bg-accent group-hover/item:text-accent-foreground",
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
isActive && "bg-accent text-accent-foreground",
isBusy && "opacity-50 pointer-events-none"
)}
>
<span className="truncate">{thread.title || "New Chat"}</span>
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" align="start">
<p>
{t("updated") || "Updated"}: {formatThreadTimestamp(thread.updatedAt)}
</p>
</TooltipContent>
</Tooltip>
)}
<div
className={cn(
"pointer-events-none absolute right-0 top-0 bottom-0 flex items-center rounded-r-md pl-6 pr-1",
isActive
? "bg-gradient-to-l from-accent from-60% to-transparent"
: "bg-gradient-to-l from-sidebar from-60% to-transparent group-hover/item:from-accent",
isMobile
? "opacity-0"
: openDropdownId === thread.id
? "opacity-100"
: "opacity-0 group-hover/item:opacity-100"
)}
>
<DropdownMenu
open={openDropdownId === thread.id}
onOpenChange={(isOpen) => setOpenDropdownId(isOpen ? thread.id : null)}
>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
className={cn(
"pointer-events-auto h-6 w-6 hover:bg-transparent",
openDropdownId === thread.id && "bg-accent hover:bg-accent"
)}
disabled={isBusy}
>
{isDeleting ? (
<Spinner size="xs" />
) : (
<MoreHorizontal className="h-3.5 w-3.5 text-muted-foreground" />
)}
<span className="sr-only">{t("more_options") || "More options"}</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-40 z-80">
{!thread.archived && (
<DropdownMenuItem
onClick={() => handleStartRename(thread.id, thread.title || "New Chat")}
>
<Pencil className="mr-2 h-4 w-4" />
<span>{t("rename") || "Rename"}</span>
</DropdownMenuItem>
)}
<DropdownMenuItem
onClick={() => handleToggleArchive(thread.id, thread.archived)}
disabled={isArchiving}
>
{thread.archived ? (
<>
<RotateCcwIcon className="mr-2 h-4 w-4" />
<span>{t("unarchive") || "Restore"}</span>
</>
) : (
<>
<ArchiveIcon className="mr-2 h-4 w-4" />
<span>{t("archive") || "Archive"}</span>
</>
)}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleDeleteThread(thread.id)}>
<Trash2 className="mr-2 h-4 w-4" />
<span>{t("delete") || "Delete"}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
);
})}
</div>
) : isSearchMode ? (
<div className="text-center py-8">
<Search className="mx-auto mb-2.5 h-10 w-10 text-muted-foreground" />
<p className="text-xs text-muted-foreground">
{t("no_chats_found") || "No chats found"}
</p>
<p className="mt-1 text-[11px] text-muted-foreground/70">
{t("try_different_search") || "Try a different search term"}
</p>
</div>
) : (
<div className="text-center py-8">
<Users className="mx-auto mb-2.5 h-10 w-10 text-muted-foreground" />
<p className="text-xs text-muted-foreground">
{showArchived
? t("no_archived_chats") || "No archived chats"
: t("no_shared_chats") || "No shared chats"}
</p>
{!showArchived && (
<p className="mt-1 text-[11px] text-muted-foreground/70">
Share a chat to collaborate with your team
</p>
)}
</div>
)}
</div>
<Dialog open={showRenameDialog} onOpenChange={setShowRenameDialog}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<span>{t("rename_chat") || "Rename Chat"}</span>
</DialogTitle>
<DialogDescription>
{t("rename_chat_description") || "Enter a new name for this conversation."}
</DialogDescription>
</DialogHeader>
<Input
value={newTitle}
onChange={(e) => setNewTitle(e.target.value)}
placeholder={t("chat_title_placeholder") || "Chat title"}
onKeyDown={(e) => {
if (e.key === "Enter" && !isRenaming && newTitle.trim()) {
handleConfirmRename();
}
}}
/>
<DialogFooter className="flex sm:justify-end">
<Button
variant="secondary"
onClick={() => setShowRenameDialog(false)}
disabled={isRenaming}
>
{t("cancel")}
</Button>
<Button
onClick={handleConfirmRename}
disabled={isRenaming || !newTitle.trim()}
className="gap-2"
>
{isRenaming ? (
<>
<Spinner size="xs" />
<span>{t("renaming") || "Renaming"}</span>
</>
) : (
<span>{t("rename") || "Rename"}</span>
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}
export function AllSharedChatsSidebar({
open,
onOpenChange,
searchSpaceId,
onCloseMobileSidebar,
}: AllSharedChatsSidebarProps) {
const t = useTranslations("sidebar");
return (
<SidebarSlideOutPanel
open={open}
onOpenChange={onOpenChange}
ariaLabel={t("shared_chats") || "Shared Chats"}
>
<AllSharedChatsSidebarContent
onOpenChange={onOpenChange}
searchSpaceId={searchSpaceId}
onCloseMobileSidebar={onCloseMobileSidebar}
/>
</SidebarSlideOutPanel>
);
}

View file

@ -1,6 +1,6 @@
"use client"; "use client";
import { ArchiveIcon, MoreHorizontal, Pencil, RotateCcwIcon, Trash2 } from "lucide-react"; import { ArchiveIcon, MoreHorizontal, Pencil, RotateCcwIcon, Trash2, Users } from "lucide-react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@ -18,6 +18,7 @@ import { cn } from "@/lib/utils";
interface ChatListItemProps { interface ChatListItemProps {
name: string; name: string;
isActive?: boolean; isActive?: boolean;
isShared?: boolean;
archived?: boolean; archived?: boolean;
dropdownOpen?: boolean; dropdownOpen?: boolean;
onDropdownOpenChange?: (open: boolean) => void; onDropdownOpenChange?: (open: boolean) => void;
@ -30,6 +31,7 @@ interface ChatListItemProps {
export function ChatListItem({ export function ChatListItem({
name, name,
isActive, isActive,
isShared,
archived, archived,
dropdownOpen: controlledOpen, dropdownOpen: controlledOpen,
onDropdownOpenChange, onDropdownOpenChange,
@ -68,7 +70,13 @@ export function ChatListItem({
isActive && "bg-accent text-accent-foreground" isActive && "bg-accent text-accent-foreground"
)} )}
> >
<span className="truncate">{animatedName}</span> <span className="min-w-0 flex-1 truncate">{animatedName}</span>
{isShared ? (
<Users
aria-label={t("shared_chat") || "Shared chat"}
className="h-3 w-3 shrink-0 text-muted-foreground/50"
/>
) : null}
</Button> </Button>
{/* Actions dropdown - trigger hidden on mobile, long-press opens it instead */} {/* Actions dropdown - trigger hidden on mobile, long-press opens it instead */}

View file

@ -19,17 +19,14 @@ interface MobileSidebarProps {
navItems: NavItem[]; navItems: NavItem[];
onNavItemClick?: (item: NavItem) => void; onNavItemClick?: (item: NavItem) => void;
chats: ChatItem[]; chats: ChatItem[];
sharedChats?: ChatItem[];
activeChatId?: number | null; activeChatId?: number | null;
onNewChat: () => void; onNewChat: () => void;
onChatSelect: (chat: ChatItem) => void; onChatSelect: (chat: ChatItem) => void;
onChatRename?: (chat: ChatItem) => void; onChatRename?: (chat: ChatItem) => void;
onChatDelete?: (chat: ChatItem) => void; onChatDelete?: (chat: ChatItem) => void;
onChatArchive?: (chat: ChatItem) => void; onChatArchive?: (chat: ChatItem) => void;
onViewAllSharedChats?: () => void; onViewAllChats?: () => void;
onViewAllPrivateChats?: () => void; isChatsPanelOpen?: boolean;
isSharedChatsPanelOpen?: boolean;
isPrivateChatsPanelOpen?: boolean;
user: User; user: User;
onSettings?: () => void; onSettings?: () => void;
onManageMembers?: () => void; onManageMembers?: () => void;
@ -69,17 +66,14 @@ export function MobileSidebar({
navItems, navItems,
onNavItemClick, onNavItemClick,
chats, chats,
sharedChats,
activeChatId, activeChatId,
onNewChat, onNewChat,
onChatSelect, onChatSelect,
onChatRename, onChatRename,
onChatDelete, onChatDelete,
onChatArchive, onChatArchive,
onViewAllSharedChats, onViewAllChats,
onViewAllPrivateChats, isChatsPanelOpen = false,
isSharedChatsPanelOpen = false,
isPrivateChatsPanelOpen = false,
user, user,
onSettings, onSettings,
onManageMembers, onManageMembers,
@ -152,7 +146,6 @@ export function MobileSidebar({
navItems={navItems} navItems={navItems}
onNavItemClick={handleNavItemClick} onNavItemClick={handleNavItemClick}
chats={chats} chats={chats}
sharedChats={sharedChats}
activeChatId={activeChatId} activeChatId={activeChatId}
onNewChat={() => { onNewChat={() => {
onNewChat(); onNewChat();
@ -162,24 +155,15 @@ export function MobileSidebar({
onChatRename={onChatRename} onChatRename={onChatRename}
onChatDelete={onChatDelete} onChatDelete={onChatDelete}
onChatArchive={onChatArchive} onChatArchive={onChatArchive}
onViewAllSharedChats={ onViewAllChats={
onViewAllSharedChats onViewAllChats
? () => { ? () => {
onOpenChange(false); onOpenChange(false);
onViewAllSharedChats(); onViewAllChats();
} }
: undefined : undefined
} }
onViewAllPrivateChats={ isChatsPanelOpen={isChatsPanelOpen}
onViewAllPrivateChats
? () => {
onOpenChange(false);
onViewAllPrivateChats();
}
: undefined
}
isSharedChatsPanelOpen={isSharedChatsPanelOpen}
isPrivateChatsPanelOpen={isPrivateChatsPanelOpen}
user={user} user={user}
onSettings={ onSettings={
onSettings onSettings

View file

@ -67,17 +67,14 @@ interface SidebarProps {
navItems: NavItem[]; navItems: NavItem[];
onNavItemClick?: (item: NavItem) => void; onNavItemClick?: (item: NavItem) => void;
chats: ChatItem[]; chats: ChatItem[];
sharedChats?: ChatItem[];
activeChatId?: number | null; activeChatId?: number | null;
onNewChat: () => void; onNewChat: () => void;
onChatSelect: (chat: ChatItem) => void; onChatSelect: (chat: ChatItem) => void;
onChatRename?: (chat: ChatItem) => void; onChatRename?: (chat: ChatItem) => void;
onChatDelete?: (chat: ChatItem) => void; onChatDelete?: (chat: ChatItem) => void;
onChatArchive?: (chat: ChatItem) => void; onChatArchive?: (chat: ChatItem) => void;
onViewAllSharedChats?: () => void; onViewAllChats?: () => void;
onViewAllPrivateChats?: () => void; isChatsPanelOpen?: boolean;
isSharedChatsPanelOpen?: boolean;
isPrivateChatsPanelOpen?: boolean;
user: User; user: User;
onSettings?: () => void; onSettings?: () => void;
onManageMembers?: () => void; onManageMembers?: () => void;
@ -106,17 +103,14 @@ export function Sidebar({
navItems, navItems,
onNavItemClick, onNavItemClick,
chats, chats,
sharedChats = [],
activeChatId, activeChatId,
onNewChat, onNewChat,
onChatSelect, onChatSelect,
onChatRename, onChatRename,
onChatDelete, onChatDelete,
onChatArchive, onChatArchive,
onViewAllSharedChats, onViewAllChats,
onViewAllPrivateChats, isChatsPanelOpen = false,
isSharedChatsPanelOpen = false,
isPrivateChatsPanelOpen = false,
user, user,
onSettings, onSettings,
onManageMembers, onManageMembers,
@ -264,73 +258,20 @@ export function Sidebar({
<div className="flex-1 w-full" /> <div className="flex-1 w-full" />
) : ( ) : (
<div className="flex-1 flex flex-col gap-1 pt-2 w-full min-h-0 overflow-hidden"> <div className="flex-1 flex flex-col gap-1 pt-2 w-full min-h-0 overflow-hidden">
{/* Shared Chats Section - takes only space needed, max 50% */}
<SidebarSection
title={t("shared_chats")}
defaultOpen={true}
fillHeight={false}
className="shrink-0 max-h-[50%] flex flex-col"
alwaysShowAction={!disableTooltips && isSharedChatsPanelOpen}
action={
onViewAllSharedChats ? (
<Button
type="button"
variant="ghost"
onClick={onViewAllSharedChats}
className="h-auto cursor-pointer whitespace-nowrap bg-transparent p-0 text-xs font-medium text-muted-foreground/60 transition-colors hover:bg-transparent hover:text-muted-foreground"
>
{!disableTooltips && isSharedChatsPanelOpen ? t("hide") : t("show_all")}
</Button>
) : undefined
}
>
{isLoadingChats ? (
<ChatListSkeletonRows />
) : sharedChats.length > 0 ? (
<div className="relative min-h-0 flex-1">
<div
className={`flex flex-col gap-0.5 max-h-full overflow-y-auto scrollbar-thin scrollbar-thumb-muted-foreground/20 scrollbar-track-transparent ${sharedChats.length > 4 ? "pb-2" : ""}`}
>
{sharedChats.slice(0, 20).map((chat) => (
<ChatListItem
key={chat.id}
name={chat.name}
isActive={chat.id === activeChatId}
archived={chat.archived}
dropdownOpen={openDropdownChatId === chat.id}
onDropdownOpenChange={(open) => setOpenDropdownChatId(open ? chat.id : null)}
onClick={() => onChatSelect(chat)}
onRename={() => onChatRename?.(chat)}
onArchive={() => onChatArchive?.(chat)}
onDelete={() => onChatDelete?.(chat)}
/>
))}
</div>
{/* Gradient fade indicator when more than 4 items */}
{sharedChats.length > 4 && (
<div className="pointer-events-none absolute bottom-0 left-0 right-0 h-5 bg-gradient-to-t from-sidebar/80 to-transparent" />
)}
</div>
) : (
<p className="px-2 py-1 text-sm text-muted-foreground/60">{t("no_shared_chats")}</p>
)}
</SidebarSection>
{/* Private Chats Section - fills remaining space */}
<SidebarSection <SidebarSection
title={t("chats")} title={t("chats")}
defaultOpen={true} defaultOpen={true}
fillHeight={true} fillHeight={true}
alwaysShowAction={!disableTooltips && isPrivateChatsPanelOpen} alwaysShowAction={!disableTooltips && isChatsPanelOpen}
action={ action={
onViewAllPrivateChats ? ( onViewAllChats ? (
<Button <Button
type="button" type="button"
variant="ghost" variant="ghost"
onClick={onViewAllPrivateChats} onClick={onViewAllChats}
className="h-auto cursor-pointer whitespace-nowrap bg-transparent p-0 text-xs font-medium text-muted-foreground/60 transition-colors hover:bg-transparent hover:text-muted-foreground" className="h-auto cursor-pointer whitespace-nowrap bg-transparent p-0 text-xs font-medium text-muted-foreground/60 transition-colors hover:bg-transparent hover:text-muted-foreground"
> >
{!disableTooltips && isPrivateChatsPanelOpen ? t("hide") : t("show_all")} {!disableTooltips && isChatsPanelOpen ? t("hide") : t("show_all")}
</Button> </Button>
) : undefined ) : undefined
} }
@ -347,6 +288,7 @@ export function Sidebar({
key={chat.id} key={chat.id}
name={chat.name} name={chat.name}
isActive={chat.id === activeChatId} isActive={chat.id === activeChatId}
isShared={chat.visibility === "SEARCH_SPACE"}
archived={chat.archived} archived={chat.archived}
dropdownOpen={openDropdownChatId === chat.id} dropdownOpen={openDropdownChatId === chat.id}
onDropdownOpenChange={(open) => setOpenDropdownChatId(open ? chat.id : null)} onDropdownOpenChange={(open) => setOpenDropdownChatId(open ? chat.id : null)}

View file

@ -1,5 +1,4 @@
export { AllPrivateChatsSidebar, AllPrivateChatsSidebarContent } from "./AllPrivateChatsSidebar"; export { AllChatsSidebar, AllChatsSidebarContent } from "./AllChatsSidebar";
export { AllSharedChatsSidebar, AllSharedChatsSidebarContent } from "./AllSharedChatsSidebar";
export { ChatListItem } from "./ChatListItem"; export { ChatListItem } from "./ChatListItem";
export { DocumentsSidebar } from "./DocumentsSidebar"; export { DocumentsSidebar } from "./DocumentsSidebar";
export { InboxSidebar, InboxSidebarContent } from "./InboxSidebar"; export { InboxSidebar, InboxSidebarContent } from "./InboxSidebar";

View file

@ -650,13 +650,14 @@
"created": "Created" "created": "Created"
}, },
"sidebar": { "sidebar": {
"chats": "Private Chats", "chats": "Chats",
"shared_chats": "Shared Chats", "shared_chats": "Shared Chats",
"search_chats": "Search chats", "search_chats": "Search chats",
"no_chats_found": "No chats found", "no_chats_found": "No chats found",
"no_shared_chats": "No shared chats", "no_shared_chats": "No shared chats",
"shared_chat": "Shared chat",
"view_all_shared_chats": "View all shared chats", "view_all_shared_chats": "View all shared chats",
"view_all_private_chats": "View all private chats", "view_all_chats": "View all chats",
"show_all": "Show all", "show_all": "Show all",
"hide": "Hide", "hide": "Hide",
"no_chats": "No chats", "no_chats": "No chats",

View file

@ -650,13 +650,14 @@
"created": "Creado" "created": "Creado"
}, },
"sidebar": { "sidebar": {
"chats": "Chats privados", "chats": "Chats",
"shared_chats": "Chats compartidos", "shared_chats": "Chats compartidos",
"search_chats": "Buscar chats", "search_chats": "Buscar chats",
"no_chats_found": "No se encontraron chats", "no_chats_found": "No se encontraron chats",
"no_shared_chats": "No hay chats compartidos", "no_shared_chats": "No hay chats compartidos",
"shared_chat": "Chat compartido",
"view_all_shared_chats": "Ver todos los chats compartidos", "view_all_shared_chats": "Ver todos los chats compartidos",
"view_all_private_chats": "Ver todos los chats privados", "view_all_chats": "Ver todos los chats",
"show_all": "Ver todo", "show_all": "Ver todo",
"hide": "Ocultar", "hide": "Ocultar",
"no_chats": "Sin chats", "no_chats": "Sin chats",

View file

@ -650,13 +650,14 @@
"created": "बनाया गया" "created": "बनाया गया"
}, },
"sidebar": { "sidebar": {
"chats": "निजी चैट", "chats": "चैट",
"shared_chats": "साझा चैट", "shared_chats": "साझा चैट",
"search_chats": "चैट खोजें", "search_chats": "चैट खोजें",
"no_chats_found": "कोई चैट नहीं मिला", "no_chats_found": "कोई चैट नहीं मिला",
"no_shared_chats": "कोई साझा चैट नहीं", "no_shared_chats": "कोई साझा चैट नहीं",
"shared_chat": "साझा चैट",
"view_all_shared_chats": "सभी साझा चैट देखें", "view_all_shared_chats": "सभी साझा चैट देखें",
"view_all_private_chats": "सभी निजी चैट देखें", "view_all_chats": "सभी चैट देखें",
"show_all": "सभी देखें", "show_all": "सभी देखें",
"hide": "छिपाएँ", "hide": "छिपाएँ",
"no_chats": "कोई चैट नहीं", "no_chats": "कोई चैट नहीं",

View file

@ -650,13 +650,14 @@
"created": "Criado" "created": "Criado"
}, },
"sidebar": { "sidebar": {
"chats": "Chats privados", "chats": "Chats",
"shared_chats": "Chats compartilhados", "shared_chats": "Chats compartilhados",
"search_chats": "Pesquisar chats", "search_chats": "Pesquisar chats",
"no_chats_found": "Nenhum chat encontrado", "no_chats_found": "Nenhum chat encontrado",
"no_shared_chats": "Nenhum chat compartilhado", "no_shared_chats": "Nenhum chat compartilhado",
"shared_chat": "Chat compartilhado",
"view_all_shared_chats": "Ver todos os chats compartilhados", "view_all_shared_chats": "Ver todos os chats compartilhados",
"view_all_private_chats": "Ver todos os chats privados", "view_all_chats": "Ver todos os chats",
"show_all": "Ver tudo", "show_all": "Ver tudo",
"hide": "Ocultar", "hide": "Ocultar",
"no_chats": "Nenhum chat", "no_chats": "Nenhum chat",

View file

@ -634,13 +634,14 @@
"created": "创建于" "created": "创建于"
}, },
"sidebar": { "sidebar": {
"chats": "私人对话", "chats": "对话",
"shared_chats": "共享对话", "shared_chats": "共享对话",
"search_chats": "搜索对话...", "search_chats": "搜索对话...",
"no_chats_found": "未找到对话", "no_chats_found": "未找到对话",
"no_shared_chats": "暂无共享对话", "no_shared_chats": "暂无共享对话",
"shared_chat": "共享对话",
"view_all_shared_chats": "查看所有共享对话", "view_all_shared_chats": "查看所有共享对话",
"view_all_private_chats": "查看所有私人对话", "view_all_chats": "查看所有对话",
"show_all": "查看全部", "show_all": "查看全部",
"hide": "隐藏", "hide": "隐藏",
"no_chats": "无对话", "no_chats": "无对话",