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

View file

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

View file

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

View file

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

View file

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

View file

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