"use client"; import { useAtomValue } from "jotai"; import dynamic from "next/dynamic"; import { useMemo, useState } from "react"; import { activeTabIdAtom } from "@/atoms/tabs/tabs.atom"; import { Logo } from "@/components/Logo"; import { Spinner } from "@/components/ui/spinner"; import { TooltipProvider } from "@/components/ui/tooltip"; import { useIsMobile } from "@/hooks/use-mobile"; import { useElectronAPI } from "@/hooks/use-platform"; import { type ResolvedTab, useResolvedTabs } from "@/hooks/use-resolved-tabs"; import { cn } from "@/lib/utils"; import { SidebarProvider, useSidebarState } from "../../hooks"; import { SIDEBAR_MAX_WIDTH, SIDEBAR_MIN_WIDTH, useSidebarResize, } from "../../hooks/useSidebarResize"; import type { ChatItem, NavItem, PageUsage, User, Workspace } from "../../types/layout.types"; import { Header } from "../header"; import { IconRail } from "../icon-rail"; import { MobileDocumentsWorkspaceView } from "../right-panel/MobileDocumentsWorkspaceView"; import { RightPanel, RightPanelToggleButton } from "../right-panel/RightPanel"; import { MobileSidebar, MobileSidebarTrigger, Sidebar, SidebarCollapseButton } from "../sidebar"; import type { NotificationsDropdownData } from "../sidebar/NotificationsDropdown"; import { TabBar } from "../tabs/TabBar"; import { WorkspacePanel } from "./WorkspacePanel"; const DocumentTabContent = dynamic( () => import("../tabs/DocumentTabContent").then((m) => ({ default: m.DocumentTabContent })), { ssr: false, loading: () => (
), } ); const PLAYGROUND_SIDEBAR_COLLAPSED_COOKIE = "surfsense_playground_sidebar_collapsed"; const PLAYGROUND_SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 365; function persistPlaygroundSidebarCollapsedCookie(isCollapsed: boolean) { void window.cookieStore ?.set({ name: PLAYGROUND_SIDEBAR_COLLAPSED_COOKIE, value: String(isCollapsed), path: "/", expires: Date.now() + PLAYGROUND_SIDEBAR_COOKIE_MAX_AGE * 1000, sameSite: "lax", }) .catch(() => { // Ignore preference persistence failures. }); } function MacDesktopTitleBar({ isSidebarCollapsed, onToggleSidebar, disableRightPanelToggle = false, }: { isSidebarCollapsed: boolean; onToggleSidebar: () => void; disableRightPanelToggle?: boolean; }) { return (
); } interface LayoutShellProps { workspaces: Workspace[]; activeWorkspaceId: number | null; onWorkspaceSelect: (id: number) => void; onWorkspaceDelete?: (workspace: Workspace) => void; onWorkspaceSettings?: (workspace: Workspace) => void; onAddWorkspace: () => void; isAtWorkspaceLimit?: boolean; maxWorkspacesPerUser?: number; workspace: Workspace | null; navItems: NavItem[]; onNavItemClick?: (item: NavItem) => void; chats: ChatItem[]; activeChatId?: number | null; onNewChat: () => void; onChatSelect: (chat: ChatItem) => void; onChatPrefetch?: (chat: ChatItem) => void; onChatRename?: (chat: ChatItem) => void; onChatDelete?: (chat: ChatItem) => void; onChatArchive?: (chat: ChatItem) => void; onChatsClick?: () => void; onViewAllChats?: () => void; user: User; onSettings?: () => void; onManageMembers?: () => void; onUserSettings?: () => void; onAnnouncements?: () => void; announcementUnreadCount?: number; onLogout?: () => void; pageUsage?: PageUsage; theme?: string; setTheme?: (theme: "light" | "dark" | "system") => void; defaultCollapsed?: boolean; isChatPage?: boolean; isAllChatsPage?: boolean; showTabs?: boolean; useWorkspacePanel?: boolean; workspacePanelViewportClassName?: string; workspacePanelContentClassName?: string; children: React.ReactNode; className?: string; notifications?: NotificationsDropdownData; isLoadingChats?: boolean; documentsPanel?: { open: boolean; onOpenChange: (open: boolean) => void; }; /** Anonymous mobile layouts still open Documents from local state instead of a dashboard route. */ mobileDocumentsWorkspaceFromState?: boolean; onTabSwitch?: (tab: ResolvedTab) => void; onTabPrefetch?: (tab: ResolvedTab) => void; playgroundSidebar?: React.ReactNode; initialPlaygroundSidebarCollapsed?: boolean; } function MainContentPanel({ isChatPage, onTabSwitch, onTabPrefetch, onNewChat, showTabs = true, reserveRightPanelToggleSpace = true, showTopBorder = false, children, }: { isChatPage: boolean; onTabSwitch?: (tab: ResolvedTab) => void; onTabPrefetch?: (tab: ResolvedTab) => void; onNewChat?: () => void; showTabs?: boolean; reserveRightPanelToggleSpace?: boolean; showTopBorder?: boolean; children: React.ReactNode; }) { if (!showTabs) { return ( {children} ); } return ( {children} ); } function UntabbedMainContentPanel({ isChatPage, showTopBorder, children, }: { isChatPage: boolean; showTopBorder: boolean; children: React.ReactNode; }) { return (
{children}
); } function TabbedMainContentPanel({ isChatPage, onTabSwitch, onTabPrefetch, onNewChat, reserveRightPanelToggleSpace, showTopBorder, children, }: { isChatPage: boolean; onTabSwitch?: (tab: ResolvedTab) => void; onTabPrefetch?: (tab: ResolvedTab) => void; onNewChat?: () => void; reserveRightPanelToggleSpace: boolean; showTopBorder: boolean; children: React.ReactNode; }) { const activeTabId = useAtomValue(activeTabIdAtom); const tabs = useResolvedTabs(); const activeTab = tabs.find((tab) => tab.id === activeTabId) ?? null; const isDocumentTab = activeTab?.type === "document"; return (
); } function DesktopWorkspaceRegion({ children }: { children: React.ReactNode }) { return
{children}
; } export function LayoutShell({ workspaces, activeWorkspaceId, onWorkspaceSelect, onWorkspaceDelete, onWorkspaceSettings, onAddWorkspace, isAtWorkspaceLimit = false, maxWorkspacesPerUser, workspace, navItems, onNavItemClick, chats, activeChatId, onNewChat, onChatSelect, onChatPrefetch, onChatRename, onChatDelete, onChatArchive, onChatsClick, onViewAllChats, user, onSettings, onManageMembers, onUserSettings, onAnnouncements, announcementUnreadCount = 0, onLogout, pageUsage, theme, setTheme, defaultCollapsed = false, isChatPage = false, isAllChatsPage = false, showTabs = true, useWorkspacePanel = false, workspacePanelViewportClassName, workspacePanelContentClassName, children, className, notifications, isLoadingChats = false, documentsPanel, mobileDocumentsWorkspaceFromState = false, onTabSwitch, onTabPrefetch, playgroundSidebar, initialPlaygroundSidebarCollapsed = false, }: LayoutShellProps) { const isMobile = useIsMobile(); const electronAPI = useElectronAPI(); const isMacDesktop = electronAPI?.versions.platform === "darwin"; const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const [isPlaygroundSidebarCollapsed, setIsPlaygroundSidebarCollapsed] = useState( initialPlaygroundSidebarCollapsed ); const { isCollapsed, setIsCollapsed, toggleCollapsed } = useSidebarState(defaultCollapsed); const { sidebarWidth, handlePointerDown: onResizePointerDown, isDragging: isResizing, } = useSidebarResize(); // Memoize context value to prevent unnecessary re-renders const sidebarContextValue = useMemo( () => ({ isCollapsed, setIsCollapsed, toggleCollapsed }), [isCollapsed, setIsCollapsed, toggleCollapsed] ); const handlePlaygroundSidebarToggle = () => { setIsPlaygroundSidebarCollapsed((collapsed) => { const nextCollapsed = !collapsed; persistPlaygroundSidebarCollapsedCookie(nextCollapsed); return nextCollapsed; }); }; const closeMobileDocuments = () => { if (mobileDocumentsWorkspaceFromState) documentsPanel?.onOpenChange(false); }; // Mobile layout if (isMobile) { return (
setMobileMenuOpen(true)} />} /> { closeMobileDocuments(); onWorkspaceSelect(id); }} onAddWorkspace={onAddWorkspace} isAtWorkspaceLimit={isAtWorkspaceLimit} maxWorkspacesPerUser={maxWorkspacesPerUser} workspace={workspace} navItems={navItems} onNavItemClick={(item) => { if (item.url !== "#documents") closeMobileDocuments(); onNavItemClick?.(item); }} chats={chats} activeChatId={activeChatId} onNewChat={() => { closeMobileDocuments(); onNewChat(); }} onChatSelect={(chat) => { closeMobileDocuments(); onChatSelect(chat); }} onChatPrefetch={onChatPrefetch} onChatRename={onChatRename} onChatDelete={onChatDelete} onChatArchive={onChatArchive} onChatsClick={ onChatsClick ? () => { closeMobileDocuments(); onChatsClick(); } : undefined } onViewAllChats={ onViewAllChats ? () => { closeMobileDocuments(); onViewAllChats(); } : undefined } isAllChatsActive={isAllChatsPage} user={user} onSettings={onSettings} onManageMembers={onManageMembers} onUserSettings={onUserSettings} onAnnouncements={onAnnouncements} announcementUnreadCount={announcementUnreadCount} notifications={notifications} onLogout={onLogout} pageUsage={pageUsage} theme={theme} setTheme={setTheme} isLoadingChats={isLoadingChats} /> {mobileDocumentsWorkspaceFromState && documentsPanel?.open ? ( ) : useWorkspacePanel ? ( {children} ) : (
{children}
)}
); } // Desktop layout return (
{isMacDesktop ? ( ) : null}
{/* Sidebar + slide-out panels share one container; overflow visible so panels can overlay main content. Negative right margin closes the flex gap so the sidebar sits flush against the main panel, separated only by a border. */} {playgroundSidebar ? (
{playgroundSidebar}
) : null} {useWorkspacePanel ? ( {children} ) : ( <> {/* Main content panel */} {children} {/* Right panel — tabbed Documents/Report/Editor/Citations/Artifacts (desktop only) */} {!isMacDesktop && (
)} )}
); }