"use client"; import { useAtomValue } from "jotai"; import { AnimatePresence, motion } from "motion/react"; import dynamic from "next/dynamic"; import { useCallback, useMemo, useState } from "react"; import { activeTabAtom, type Tab } from "@/atoms/tabs/tabs.atom"; import { Logo } from "@/components/Logo"; import { Spinner } from "@/components/ui/spinner"; import { TooltipProvider } from "@/components/ui/tooltip"; import type { InboxItem } from "@/hooks/use-inbox"; import { useIsMobile } from "@/hooks/use-mobile"; import { useElectronAPI } from "@/hooks/use-platform"; 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 { RightPanel, RightPanelExpandButton, RightPanelToggleButton, } from "../right-panel/RightPanel"; import { InboxSidebarContent, MobileSidebar, MobileSidebarTrigger, Sidebar, SidebarCollapseButton, } from "../sidebar"; import { SidebarSlideOutPanel } from "../sidebar/SidebarSlideOutPanel"; import { TabBar } from "../tabs/TabBar"; import { WorkspacePanel } from "./WorkspacePanel"; const DocumentTabContent = dynamic( () => import("../tabs/DocumentTabContent").then((m) => ({ default: m.DocumentTabContent })), { ssr: false, loading: () => (
), } ); function MacDesktopTitleBar({ isSidebarCollapsed, onToggleSidebar, disableRightPanelToggle = false, }: { isSidebarCollapsed: boolean; onToggleSidebar: () => void; disableRightPanelToggle?: boolean; }) { return (
); } // Per-tab data source interface TabDataSource { items: InboxItem[]; unreadCount: number; loading: boolean; loadingMore: boolean; hasMore: boolean; loadMore: () => void; markAsRead: (id: number) => Promise; markAllAsRead: () => Promise; } export type ActiveSlideoutPanel = "inbox" | null; // Inbox-related props — per-tab data sources with independent loading/pagination interface InboxProps { isOpen: boolean; totalUnreadCount: number; comments: TabDataSource; status: TabDataSource; } interface LayoutShellProps { workspaces: Workspace[]; activeWorkspaceId: number | null; onWorkspaceSelect: (id: number) => void; onWorkspaceDelete?: (workspace: Workspace) => void; onWorkspaceSettings?: (workspace: Workspace) => void; onAddWorkspace: () => void; 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; 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; useWorkspacePanel?: boolean; workspacePanelViewportClassName?: string; workspacePanelContentClassName?: string; children: React.ReactNode; className?: string; // Unified slide-out panel state activeSlideoutPanel?: ActiveSlideoutPanel; onSlideoutPanelChange?: (panel: ActiveSlideoutPanel) => void; // Inbox props inbox?: InboxProps; isLoadingChats?: boolean; documentsPanel?: { open: boolean; onOpenChange: (open: boolean) => void; }; onTabSwitch?: (tab: Tab) => void; onTabPrefetch?: (tab: Tab) => void; } function MainContentPanel({ isChatPage, onTabSwitch, onTabPrefetch, onNewChat, showRightPanelExpandButton = true, showTopBorder = false, children, }: { isChatPage: boolean; onTabSwitch?: (tab: Tab) => void; onTabPrefetch?: (tab: Tab) => void; onNewChat?: () => void; showRightPanelExpandButton?: boolean; showTopBorder?: boolean; children: React.ReactNode; }) { const activeTab = useAtomValue(activeTabAtom); const isDocumentTab = activeTab?.type === "document"; return (
: null} className="min-w-0" />
{isDocumentTab && activeTab.documentId && activeTab.workspaceId ? (
) : (
{children}
)}
); } function DesktopWorkspaceRegion({ children }: { children: React.ReactNode }) { return
{children}
; } export function LayoutShell({ workspaces, activeWorkspaceId, onWorkspaceSelect, onWorkspaceDelete, onWorkspaceSettings, onAddWorkspace, workspace, navItems, onNavItemClick, chats, activeChatId, onNewChat, onChatSelect, onChatPrefetch, onChatRename, onChatDelete, onChatArchive, onViewAllChats, user, onSettings, onManageMembers, onUserSettings, onAnnouncements, announcementUnreadCount = 0, onLogout, pageUsage, theme, setTheme, defaultCollapsed = false, isChatPage = false, isAllChatsPage = false, useWorkspacePanel = false, workspacePanelViewportClassName, workspacePanelContentClassName, children, className, activeSlideoutPanel = null, onSlideoutPanelChange, inbox, isLoadingChats = false, documentsPanel, onTabSwitch, onTabPrefetch, }: LayoutShellProps) { const isMobile = useIsMobile(); const electronAPI = useElectronAPI(); const isMacDesktop = electronAPI?.versions.platform === "darwin"; const [mobileMenuOpen, setMobileMenuOpen] = useState(false); 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 closeSlideout = useCallback( (open: boolean) => { if (!open) onSlideoutPanelChange?.(null); }, [onSlideoutPanelChange] ); const anySlideOutOpen = activeSlideoutPanel !== null; const panelAriaLabel = activeSlideoutPanel === "inbox" ? "Inbox" : "Panel"; // Mobile layout if (isMobile) { return (
setMobileMenuOpen(true)} />} /> {useWorkspacePanel ? ( {children} ) : (
{children}
)} {/* Mobile unified slide-out panel */} {activeSlideoutPanel === "inbox" && inbox && ( closeSlideout(open)} comments={inbox.comments} status={inbox.status} totalUnreadCount={inbox.totalUnreadCount} onCloseMobileSidebar={() => setMobileMenuOpen(false)} /> )}
); } // 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. */} {useWorkspacePanel ? ( {children} ) : ( <> {/* Main content panel */} {children} {/* Right panel — Report/Editor/Citations/Artifacts (desktop only) */} )}
); }