feat(chats): implement ChatsPage component and update layout for all chats functionality

This commit is contained in:
Anish Sarkar 2026-07-06 10:26:32 +05:30
parent 248e3aae57
commit 420ce3e153
7 changed files with 57 additions and 119 deletions

View file

@ -0,0 +1,11 @@
import { AllChatsWorkspaceContent } from "@/components/layout/ui/sidebar";
export default async function ChatsPage({ params }: { params: Promise<{ workspace_id: string }> }) {
const { workspace_id } = await params;
return (
<div className="w-full select-none">
<AllChatsWorkspaceContent searchSpaceId={workspace_id} />
</div>
);
}

View file

@ -126,7 +126,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" | "chats" | null; type SlideoutPanel = "inbox" | null;
const [activeSlideoutPanel, setActiveSlideoutPanel] = useState<SlideoutPanel>(null); const [activeSlideoutPanel, setActiveSlideoutPanel] = useState<SlideoutPanel>(null);
const isInboxSidebarOpen = activeSlideoutPanel === "inbox"; const isInboxSidebarOpen = activeSlideoutPanel === "inbox";
@ -631,10 +631,6 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
} }
}, [router]); }, [router]);
const handleViewAllChats = useCallback(() => {
setActiveSlideoutPanel((prev) => (prev === "chats" ? null : "chats"));
}, []);
// Delete handlers // Delete handlers
const confirmDeleteChat = useCallback(async () => { const confirmDeleteChat = useCallback(async () => {
if (!chatToDelete) return; if (!chatToDelete) return;
@ -713,6 +709,13 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
const isSearchSpaceSettingsPage = pathname?.includes("/search-space-settings") === true; const isSearchSpaceSettingsPage = pathname?.includes("/search-space-settings") === true;
const isTeamPage = pathname?.endsWith("/team") === true; const isTeamPage = pathname?.endsWith("/team") === true;
const isAutomationsPage = pathname?.includes("/automations") === true; const isAutomationsPage = pathname?.includes("/automations") === true;
const isAllChatsPage = pathname?.endsWith("/chats") === true;
const handleViewAllChats = useCallback(() => {
setActiveSlideoutPanel(null);
router.push(
isAllChatsPage ? `/dashboard/${searchSpaceId}/new-chat` : `/dashboard/${searchSpaceId}/chats`
);
}, [isAllChatsPage, router, searchSpaceId]);
const useWorkspacePanel = const useWorkspacePanel =
pathname?.endsWith("/buy-more") === true || pathname?.endsWith("/buy-more") === true ||
pathname?.endsWith("/earn-credits") === true || pathname?.endsWith("/earn-credits") === true ||
@ -720,7 +723,8 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
isUserSettingsPage || isUserSettingsPage ||
isSearchSpaceSettingsPage || isSearchSpaceSettingsPage ||
isTeamPage || isTeamPage ||
isAutomationsPage; isAutomationsPage ||
isAllChatsPage;
return ( return (
<> <>
@ -757,18 +761,25 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
theme={theme} theme={theme}
setTheme={setTheme} setTheme={setTheme}
isChatPage={isChatPage} isChatPage={isChatPage}
isAllChatsPage={isAllChatsPage}
useWorkspacePanel={useWorkspacePanel} useWorkspacePanel={useWorkspacePanel}
workspacePanelViewportClassName={ workspacePanelViewportClassName={
isUserSettingsPage || isSearchSpaceSettingsPage || isTeamPage || isAutomationsPage isUserSettingsPage ||
isSearchSpaceSettingsPage ||
isTeamPage ||
isAutomationsPage ||
isAllChatsPage
? "items-start justify-center px-6 py-8 md:px-10 md:py-10" ? "items-start justify-center px-6 py-8 md:px-10 md:py-10"
: undefined : undefined
} }
workspacePanelContentClassName={ workspacePanelContentClassName={
isAutomationsPage isAutomationsPage
? "max-w-none select-none" ? "max-w-none select-none"
: isUserSettingsPage || isSearchSpaceSettingsPage || isTeamPage : isAllChatsPage
? "max-w-5xl" ? "max-w-3xl"
: undefined : isUserSettingsPage || isSearchSpaceSettingsPage || isTeamPage
? "max-w-5xl"
: undefined
} }
isLoadingChats={isLoadingThreads} isLoadingChats={isLoadingThreads}
activeSlideoutPanel={activeSlideoutPanel} activeSlideoutPanel={activeSlideoutPanel}
@ -797,9 +808,6 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
markAllAsRead: statusInbox.markAllAsRead, markAllAsRead: statusInbox.markAllAsRead,
}, },
}} }}
allChatsPanel={{
searchSpaceId,
}}
documentsPanel={{ documentsPanel={{
open: isDocumentsSidebarOpen, open: isDocumentsSidebarOpen,
onOpenChange: setIsDocumentsSidebarOpen, onOpenChange: setIsDocumentsSidebarOpen,

View file

@ -27,7 +27,6 @@ import {
RightPanelToggleButton, RightPanelToggleButton,
} from "../right-panel/RightPanel"; } from "../right-panel/RightPanel";
import { import {
AllChatsSidebarContent,
DocumentsSidebar, DocumentsSidebar,
InboxSidebarContent, InboxSidebarContent,
MobileSidebar, MobileSidebar,
@ -93,7 +92,7 @@ interface TabDataSource {
markAllAsRead: () => Promise<boolean>; markAllAsRead: () => Promise<boolean>;
} }
export type ActiveSlideoutPanel = "inbox" | "chats" | null; export type ActiveSlideoutPanel = "inbox" | 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 {
@ -134,6 +133,7 @@ interface LayoutShellProps {
setTheme?: (theme: "light" | "dark" | "system") => void; setTheme?: (theme: "light" | "dark" | "system") => void;
defaultCollapsed?: boolean; defaultCollapsed?: boolean;
isChatPage?: boolean; isChatPage?: boolean;
isAllChatsPage?: boolean;
useWorkspacePanel?: boolean; useWorkspacePanel?: boolean;
workspacePanelViewportClassName?: string; workspacePanelViewportClassName?: string;
workspacePanelContentClassName?: string; workspacePanelContentClassName?: string;
@ -145,10 +145,6 @@ interface LayoutShellProps {
// Inbox props // Inbox props
inbox?: InboxProps; inbox?: InboxProps;
isLoadingChats?: boolean; isLoadingChats?: boolean;
// All chats panel props
allChatsPanel?: {
searchSpaceId: string;
};
documentsPanel?: { documentsPanel?: {
open: boolean; open: boolean;
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
@ -245,6 +241,7 @@ export function LayoutShell({
setTheme, setTheme,
defaultCollapsed = false, defaultCollapsed = false,
isChatPage = false, isChatPage = false,
isAllChatsPage = false,
useWorkspacePanel = false, useWorkspacePanel = false,
workspacePanelViewportClassName, workspacePanelViewportClassName,
workspacePanelContentClassName, workspacePanelContentClassName,
@ -254,7 +251,6 @@ export function LayoutShell({
onSlideoutPanelChange, onSlideoutPanelChange,
inbox, inbox,
isLoadingChats = false, isLoadingChats = false,
allChatsPanel,
documentsPanel, documentsPanel,
onTabSwitch, onTabSwitch,
onTabPrefetch, onTabPrefetch,
@ -285,8 +281,7 @@ export function LayoutShell({
const anySlideOutOpen = activeSlideoutPanel !== null; const anySlideOutOpen = activeSlideoutPanel !== null;
const panelAriaLabel = const panelAriaLabel = activeSlideoutPanel === "inbox" ? "Inbox" : "Panel";
activeSlideoutPanel === "inbox" ? "Inbox" : activeSlideoutPanel === "chats" ? "Chats" : "Panel";
// Mobile layout // Mobile layout
if (isMobile) { if (isMobile) {
@ -317,7 +312,7 @@ export function LayoutShell({
onChatDelete={onChatDelete} onChatDelete={onChatDelete}
onChatArchive={onChatArchive} onChatArchive={onChatArchive}
onViewAllChats={onViewAllChats} onViewAllChats={onViewAllChats}
isChatsPanelOpen={activeSlideoutPanel === "chats"} isAllChatsActive={isAllChatsPage}
user={user} user={user}
onSettings={onSettings} onSettings={onSettings}
onManageMembers={onManageMembers} onManageMembers={onManageMembers}
@ -369,22 +364,6 @@ export function LayoutShell({
/> />
</motion.div> </motion.div>
)} )}
{activeSlideoutPanel === "chats" && allChatsPanel && (
<motion.div
key="chats"
className="h-full flex flex-col"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.15 }}
>
<AllChatsSidebarContent
onOpenChange={(open) => closeSlideout(open)}
searchSpaceId={allChatsPanel.searchSpaceId}
onCloseMobileSidebar={() => setMobileMenuOpen(false)}
/>
</motion.div>
)}
</AnimatePresence> </AnimatePresence>
</SidebarSlideOutPanel> </SidebarSlideOutPanel>
@ -460,7 +439,7 @@ export function LayoutShell({
onChatDelete={onChatDelete} onChatDelete={onChatDelete}
onChatArchive={onChatArchive} onChatArchive={onChatArchive}
onViewAllChats={onViewAllChats} onViewAllChats={onViewAllChats}
isChatsPanelOpen={activeSlideoutPanel === "chats"} isAllChatsActive={isAllChatsPage}
user={user} user={user}
onSettings={onSettings} onSettings={onSettings}
onManageMembers={onManageMembers} onManageMembers={onManageMembers}
@ -526,21 +505,6 @@ export function LayoutShell({
/> />
</motion.div> </motion.div>
)} )}
{activeSlideoutPanel === "chats" && allChatsPanel && (
<motion.div
key="chats"
className="h-full flex flex-col"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.15 }}
>
<AllChatsSidebarContent
onOpenChange={(open) => closeSlideout(open)}
searchSpaceId={allChatsPanel.searchSpaceId}
/>
</motion.div>
)}
</AnimatePresence> </AnimatePresence>
</SidebarSlideOutPanel> </SidebarSlideOutPanel>
</div> </div>

View file

@ -4,7 +4,6 @@ import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useSetAtom } from "jotai"; import { useSetAtom } from "jotai";
import { import {
ArchiveIcon, ArchiveIcon,
ChevronLeft,
MessageCircleMore, MessageCircleMore,
MoreHorizontal, MoreHorizontal,
Pencil, Pencil,
@ -48,23 +47,13 @@ import { useArchiveThread, useDeleteThread, useRenameThread } from "@/hooks/use-
import { fetchThreads, searchThreads, type ThreadListItem } from "@/lib/chat/thread-persistence"; import { fetchThreads, searchThreads, type ThreadListItem } from "@/lib/chat/thread-persistence";
import { formatThreadTimestamp } from "@/lib/format-date"; import { formatThreadTimestamp } from "@/lib/format-date";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { SidebarSlideOutPanel } from "./SidebarSlideOutPanel";
export interface AllChatsSidebarContentProps { interface AllChatsContentProps {
onOpenChange: (open: boolean) => void;
searchSpaceId: string; searchSpaceId: string;
onCloseMobileSidebar?: () => void; className?: string;
} }
interface AllChatsSidebarProps extends AllChatsSidebarContentProps { function AllChatsContent({ searchSpaceId, className }: AllChatsContentProps) {
open: boolean;
}
export function AllChatsSidebarContent({
onOpenChange,
searchSpaceId,
onCloseMobileSidebar,
}: AllChatsSidebarContentProps) {
const t = useTranslations("sidebar"); const t = useTranslations("sidebar");
const router = useRouter(); const router = useRouter();
const params = useParams(); const params = useParams();
@ -150,10 +139,8 @@ export function AllChatsSidebarContent({
searchSpaceId, searchSpaceId,
visibility: thread.visibility, visibility: thread.visibility,
}); });
onOpenChange(false);
onCloseMobileSidebar?.();
}, },
[activateChatThread, onOpenChange, searchSpaceId, onCloseMobileSidebar] [activateChatThread, searchSpaceId]
); );
const handleDeleteThread = useCallback( const handleDeleteThread = useCallback(
@ -165,7 +152,6 @@ export function AllChatsSidebarContent({
toast.success(t("chat_deleted") || "Chat deleted successfully"); toast.success(t("chat_deleted") || "Chat deleted successfully");
if (currentChatId === threadId) { if (currentChatId === threadId) {
onOpenChange(false);
setTimeout(() => { setTimeout(() => {
if ( if (
fallbackTab?.type === "chat" && fallbackTab?.type === "chat" &&
@ -196,16 +182,7 @@ export function AllChatsSidebarContent({
setDeletingThreadId(null); setDeletingThreadId(null);
} }
}, },
[ [activateChatThread, deleteThread, t, currentChatId, router, removeChatTab, searchSpaceId]
activateChatThread,
deleteThread,
t,
currentChatId,
router,
onOpenChange,
removeChatTab,
searchSpaceId,
]
); );
const handleToggleArchive = useCallback( const handleToggleArchive = useCallback(
@ -266,20 +243,9 @@ export function AllChatsSidebarContent({
const archivedCount = archivedChats.length; const archivedCount = archivedChats.length;
return ( return (
<> <div className={cn("flex h-full min-h-0 flex-col", className)}>
<div className="shrink-0 p-3 pb-1.5 space-y-2"> <div className="shrink-0 p-3 pb-1.5 space-y-2">
<div className="flex items-center gap-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("chats") || "Chats"}</h2> <h2 className="text-lg font-semibold">{t("chats") || "Chats"}</h2>
</div> </div>
@ -584,25 +550,14 @@ export function AllChatsSidebarContent({
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
</> </div>
); );
} }
export function AllChatsSidebar({ export function AllChatsWorkspaceContent({ searchSpaceId }: { searchSpaceId: string }) {
open,
onOpenChange,
searchSpaceId,
onCloseMobileSidebar,
}: AllChatsSidebarProps) {
const t = useTranslations("sidebar");
return ( return (
<SidebarSlideOutPanel open={open} onOpenChange={onOpenChange} ariaLabel={t("chats") || "Chats"}> <div className="flex h-[min(760px,calc(100vh-8rem))] w-full overflow-hidden rounded-xl border bg-sidebar text-sidebar-foreground shadow-sm">
<AllChatsSidebarContent <AllChatsContent searchSpaceId={searchSpaceId} />
onOpenChange={onOpenChange} </div>
searchSpaceId={searchSpaceId}
onCloseMobileSidebar={onCloseMobileSidebar}
/>
</SidebarSlideOutPanel>
); );
} }

View file

@ -27,7 +27,7 @@ interface MobileSidebarProps {
onChatDelete?: (chat: ChatItem) => void; onChatDelete?: (chat: ChatItem) => void;
onChatArchive?: (chat: ChatItem) => void; onChatArchive?: (chat: ChatItem) => void;
onViewAllChats?: () => void; onViewAllChats?: () => void;
isChatsPanelOpen?: boolean; isAllChatsActive?: boolean;
user: User; user: User;
onSettings?: () => void; onSettings?: () => void;
onManageMembers?: () => void; onManageMembers?: () => void;
@ -75,7 +75,7 @@ export function MobileSidebar({
onChatDelete, onChatDelete,
onChatArchive, onChatArchive,
onViewAllChats, onViewAllChats,
isChatsPanelOpen = false, isAllChatsActive = false,
user, user,
onSettings, onSettings,
onManageMembers, onManageMembers,
@ -166,7 +166,7 @@ export function MobileSidebar({
} }
: undefined : undefined
} }
isChatsPanelOpen={isChatsPanelOpen} isAllChatsActive={isAllChatsActive}
user={user} user={user}
onSettings={ onSettings={
onSettings onSettings

View file

@ -75,7 +75,7 @@ interface SidebarProps {
onChatDelete?: (chat: ChatItem) => void; onChatDelete?: (chat: ChatItem) => void;
onChatArchive?: (chat: ChatItem) => void; onChatArchive?: (chat: ChatItem) => void;
onViewAllChats?: () => void; onViewAllChats?: () => void;
isChatsPanelOpen?: boolean; isAllChatsActive?: boolean;
user: User; user: User;
onSettings?: () => void; onSettings?: () => void;
onManageMembers?: () => void; onManageMembers?: () => void;
@ -112,7 +112,7 @@ export function Sidebar({
onChatDelete, onChatDelete,
onChatArchive, onChatArchive,
onViewAllChats, onViewAllChats,
isChatsPanelOpen = false, isAllChatsActive = false,
user, user,
onSettings, onSettings,
onManageMembers, onManageMembers,
@ -281,7 +281,7 @@ export function Sidebar({
title={t("recents")} title={t("recents")}
defaultOpen={true} defaultOpen={true}
fillHeight={true} fillHeight={true}
alwaysShowAction={!disableTooltips && isChatsPanelOpen} alwaysShowAction={!disableTooltips && isAllChatsActive}
action={ action={
onViewAllChats ? ( onViewAllChats ? (
<Button <Button
@ -290,7 +290,7 @@ export function Sidebar({
onClick={onViewAllChats} 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 && isChatsPanelOpen ? t("hide") : t("show_all")} {!disableTooltips && isAllChatsActive ? t("hide") : t("show_all")}
</Button> </Button>
) : undefined ) : undefined
} }

View file

@ -1,4 +1,4 @@
export { AllChatsSidebar, AllChatsSidebarContent } from "./AllChatsSidebar"; export { AllChatsWorkspaceContent } from "./AllChatsSidebar";
export { ChatListItem } from "./ChatListItem"; export { ChatListItem } from "./ChatListItem";
export { CreditBalanceDisplay } from "./CreditBalanceDisplay"; export { CreditBalanceDisplay } from "./CreditBalanceDisplay";
export { DocumentsSidebar } from "./DocumentsSidebar"; export { DocumentsSidebar } from "./DocumentsSidebar";