Clean chat banner (#2056)

* fully functional

* formatting

* ensure consistency with large logos

* ensure mobile support
This commit is contained in:
pablodanswer 2024-08-06 12:44:14 -07:00 committed by GitHub
parent ab564a9ec8
commit dc2a50034d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 263 additions and 250 deletions

View File

@ -4,7 +4,7 @@ import { ValidSources } from "@/lib/types";
import AddConnector from "./AddConnectorPage";
import { FormProvider } from "@/components/context/FormContext";
import Sidebar from "./Sidebar";
import { HeaderTitle } from "@/components/header/Header";
import { HeaderTitle } from "@/components/header/HeaderTitle";
import { Button } from "@tremor/react";
import { isValidSource } from "@/lib/sources";

View File

@ -1,5 +1,6 @@
import { useFormContext } from "@/components/context/FormContext";
import { HeaderTitle } from "@/components/header/Header";
import { HeaderTitle } from "@/components/header/HeaderTitle";
import { BackIcon, SettingsIcon } from "@/components/icons/icons";
import { Logo } from "@/components/Logo";
import { SettingsContext } from "@/components/settings/SettingsProvider";

View File

@ -124,6 +124,7 @@ export default function SidebarWrapper<T extends object>({
<div className="absolute h-svh left-0 w-full top-0">
<FunctionalHeader
sidebarToggled={toggledSidebar}
toggleSidebar={toggleSidebar}
page="assistants"
user={headerProps.user}

View File

@ -2,57 +2,116 @@
import ReactMarkdown from "react-markdown";
import { SettingsContext } from "@/components/settings/SettingsProvider";
import { useContext } from "react";
import { useContext, useState, useRef, useLayoutEffect } from "react";
import remarkGfm from "remark-gfm";
import { Popover } from "@/components/popover/Popover";
import { ChevronDownIcon } from "@/components/icons/icons";
import { Divider } from "@tremor/react";
export function ChatBanner() {
const settings = useContext(SettingsContext);
const [isOverflowing, setIsOverflowing] = useState(false);
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const contentRef = useRef<HTMLDivElement>(null);
const fullContentRef = useRef<HTMLDivElement>(null);
useLayoutEffect(() => {
const checkOverflow = () => {
if (contentRef.current && fullContentRef.current) {
setIsOverflowing(
fullContentRef.current.scrollHeight > contentRef.current.clientHeight
);
}
};
checkOverflow();
window.addEventListener("resize", checkOverflow);
return () => window.removeEventListener("resize", checkOverflow);
}, []);
if (!settings?.enterpriseSettings?.custom_header_content) {
return null;
}
const renderMarkdown = (className: string) => (
<ReactMarkdown
className={`w-full text-wrap break-word ${className}`}
components={{
a: ({ node, ...props }) => (
<a
{...props}
className="text-sm text-link hover:text-link-hover"
target="_blank"
rel="noopener noreferrer"
/>
),
p: ({ node, ...props }) => (
<p {...props} className="text-wrap break-word text-sm m-0 w-full" />
),
}}
remarkPlugins={[remarkGfm]}
>
{settings.enterpriseSettings?.custom_header_content}
</ReactMarkdown>
);
return (
<div
className={`
mt-8
mb-2
p-1
mx-2
px-2
z-[39]
py-1.5
text-wrap
w-[500px]
w-full
mx-auto
relative
bg-background-100
shadow-sm
rounded
border-l-8 border-l-400
border-r-4 border-r-200
border-border
border
flex`}
>
<div className="mx-auto text-emphasis text-sm flex flex-col">
<div className="my-auto">
<ReactMarkdown
className="prose flex text-wrap break-all text-wrap max-w-full"
components={{
a: ({ node, ...props }) => (
<a
{...props}
className="text-sm text-link hover:text-link-hover"
target="_blank"
rel="noopener noreferrer"
/>
),
p: ({ node, ...props }) => (
<p
{...props}
className="text-wrap break-all line-clamp-3 text-sm"
/>
),
}}
remarkPlugins={[remarkGfm]}
<div className="text-emphasis text-sm w-full">
<div className="relative">
<div
ref={contentRef}
className="line-clamp-2 text-center w-full overflow-hidden pr-8"
>
{settings.enterpriseSettings.custom_header_content}
</ReactMarkdown>
{renderMarkdown("")}
</div>
<div
ref={fullContentRef}
className="absolute top-0 left-0 invisible w-full"
>
{renderMarkdown("")}
</div>
<div className="absolute bottom-0 right-0 ">
{isOverflowing && (
<Popover
open={isPopoverOpen}
onOpenChange={setIsPopoverOpen}
content={
<button
onClick={() => setIsPopoverOpen(true)}
className="cursor-poiner bg-background-100 p-1 rounded-full"
>
<ChevronDownIcon className="h-4 w-4 text-emphasis" />
</button>
}
popover={
<div className="bg-background-100 p-4 rounded shadow-lg mobile:max-w-xs desktop:max-w-md">
<p className="text-lg font-bold">Banner Content</p>
{renderMarkdown("max-h-96 overflow-y-auto")}
</div>
}
side="bottom"
align="end"
/>
)}
</div>
</div>
</div>
</div>

View File

@ -66,7 +66,6 @@ import { useChatContext } from "@/components/context/ChatContext";
import { v4 as uuidv4 } from "uuid";
import { orderAssistantsForUser } from "@/lib/assistants/orderAssistants";
import { ChatPopup } from "./ChatPopup";
import { ChatBanner } from "./ChatBanner";
import FunctionalHeader from "@/components/chat_search/Header";
import { useSidebarVisibility } from "@/components/chat_search/hooks";
@ -1192,6 +1191,7 @@ export function ChatPage({
<div className="flex h-full flex-col w-full">
{liveAssistant && (
<FunctionalHeader
sidebarToggled={toggledSidebar}
reset={() => setMessage("")}
page="chat"
setSharingModalVisible={
@ -1204,23 +1204,7 @@ export function ChatPage({
currentChatSession={selectedChatSession}
/>
)}
<div className="w-full flex">
<div
style={{ transition: "width 0.30s ease-out" }}
className={`
flex-none
overflow-y-hidden
bg-background-100
transition-all
bg-opacity-80
duration-300
ease-in-out
h-full
${toggledSidebar ? "w-[250px]" : "w-[0px]"}
`}
/>
<ChatBanner />
</div>
{documentSidebarInitialWidth !== undefined ? (
<Dropzone onDrop={handleImageUpload} noClick>
{({ getRootProps }) => (

View File

@ -11,13 +11,9 @@ import {
} from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { BasicClickable } from "@/components/BasicClickable";
import { ChatSession } from "../interfaces";
import {
NEXT_PUBLIC_DO_NOT_USE_TOGGLE_OFF_DANSWER_POWERED,
NEXT_PUBLIC_NEW_CHAT_DIRECTS_TO_SAME_PERSONA,
} from "@/lib/constants";
import { NEXT_PUBLIC_NEW_CHAT_DIRECTS_TO_SAME_PERSONA } from "@/lib/constants";
import { Folder } from "../folders/interfaces";
import { createFolder } from "../folders/FolderManagement";
@ -25,24 +21,16 @@ import { usePopup } from "@/components/admin/connectors/Popup";
import { SettingsContext } from "@/components/settings/SettingsProvider";
import React from "react";
import { Logo } from "@/components/Logo";
import { HeaderTitle } from "@/components/header/Header";
import { TbLayoutSidebarRightExpand } from "react-icons/tb";
import {
AssistantsIcon,
AssistantsIconSkeleton,
BackIcon,
BookIcon,
BookmarkIconSkeleton,
ClosedBookIcon,
LefToLineIcon,
RightToLineIcon,
} from "@/components/icons/icons";
import { PagesTab } from "./PagesTab";
import { Tooltip } from "@/components/tooltip/Tooltip";
import KeyboardSymbol from "@/lib/browserUtilities";
import { pageType } from "./types";
import { usePaidEnterpriseFeaturesEnabled } from "@/components/settings/usePaidEnterpriseFeaturesEnabled";
import LogoType from "@/components/header/LogoType";
interface HistorySidebarProps {
page: pageType;
@ -115,43 +103,15 @@ export const HistorySidebar = forwardRef<HTMLDivElement, HistorySidebarProps>(
flex
flex-col relative
h-screen
transition-transform`}
transition-transform
mt-2`}
>
<div className="max-w-full ml-3 mr-3 mt-2 flex flex gap-x-1 items-center my-auto text-text-700 text-xl">
<div className="mr-1 desktop:invisible mb-auto h-6 w-6">
<Logo height={24} width={24} />
</div>
<div className="desktop:invisible">
{enterpriseSettings && enterpriseSettings.application_name ? (
<div>
<HeaderTitle>
{enterpriseSettings.application_name}
</HeaderTitle>
{!NEXT_PUBLIC_DO_NOT_USE_TOGGLE_OFF_DANSWER_POWERED && (
<p className="text-xs text-subtle">Powered by Danswer</p>
)}
</div>
) : (
<HeaderTitle>Danswer</HeaderTitle>
)}
</div>
{toggleSidebar && (
<Tooltip
delayDuration={0}
content={toggled ? `Unpin sidebar` : "Pin sidebar"}
>
<button className="my-auto ml-auto" onClick={toggleSidebar}>
{!toggled && !combinedSettings.isMobile ? (
<RightToLineIcon />
) : (
<LefToLineIcon />
)}
</button>
</Tooltip>
)}
</div>
<LogoType
showArrow={true}
toggled={toggled}
page={page}
toggleSidebar={toggleSidebar}
/>
{page == "chat" && (
<div className="mx-3 mt-4 gap-y-1 flex-col flex gap-x-1.5 items-center items-center">

View File

@ -7,10 +7,10 @@ import {
import { fetchSS } from "@/lib/utilsSS";
import { redirect } from "next/navigation";
import { BackendChatSession } from "../../interfaces";
import { Header } from "@/components/header/Header";
import { SharedChatDisplay } from "./SharedChatDisplay";
import { Persona } from "@/app/admin/assistants/interfaces";
import { fetchAssistantsSS } from "@/lib/assistants/fetchAssistantsSS";
import FunctionalHeader from "@/components/chat_search/Header";
async function getSharedChat(chatId: string) {
const response = await fetchSS(
@ -57,7 +57,7 @@ export default async function Page({ params }: { params: { chatId: string } }) {
return (
<div>
<div className="absolute top-0 z-40 w-full">
<Header user={user} />
<FunctionalHeader page="chat" toggleSidebar={() => null} user={user} />
</div>
<div className="flex relative bg-background text-default overflow-hidden pt-16 h-screen">

View File

@ -1,6 +1,6 @@
"use client";
import { HeaderTitle } from "@/components/header/Header";
import { HeaderTitle } from "@/components/header/HeaderTitle";
import { Logo } from "@/components/Logo";
import { SettingsContext } from "@/components/settings/SettingsProvider";
import { NEXT_PUBLIC_DO_NOT_USE_TOGGLE_OFF_DANSWER_POWERED } from "@/lib/constants";

View File

@ -115,7 +115,10 @@ export default function FunctionalWrapper({
window.removeEventListener("keydown", handleKeyDown);
};
}, [router]);
const settings = useContext(SettingsContext)?.settings;
const combinedSettings = useContext(SettingsContext);
const settings = combinedSettings?.settings;
const chatBannerPresent =
combinedSettings?.enterpriseSettings?.custom_header_content;
const [toggledSidebar, setToggledSidebar] = useState(initiallyToggled);
@ -131,7 +134,9 @@ export default function FunctionalWrapper({
<>
{(!settings ||
(settings.search_page_enabled && settings.chat_page_enabled)) && (
<div className="mobile:hidden z-30 flex fixed top-4 left-1/2 transform -translate-x-1/2">
<div
className={`mobile:hidden z-30 flex fixed ${chatBannerPresent ? "top-20" : "top-4"} left-1/2 transform -translate-x-1/2`}
>
<div
style={{ transition: "width 0.30s ease-out" }}
className={`flex-none overflow-y-hidden bg-background-100 transition-all bg-opacity-80 duration-300 ease-in-out h-full

View File

@ -1,5 +1,3 @@
import { SearchSection } from "@/components/search/SearchSection";
import { Header } from "@/components/header/Header";
import {
AuthTypeMetadata,
getAuthTypeMetadataSS,

View File

@ -1,19 +1,3 @@
import { Header } from "@/components/header/Header";
import { AdminSidebar } from "@/components/admin/connectors/AdminSidebar";
import {
NotebookIcon,
UsersIcon,
ThumbsUpIcon,
BookmarkIcon,
ZoomInIcon,
RobotIcon,
ConnectorIcon,
GroupsIcon,
DatabaseIcon,
KeyIcon,
ClipboardIcon,
BookstackIcon,
} from "@/components/icons/icons";
import { User } from "@/lib/types";
import {
AuthTypeMetadata,

View File

@ -4,7 +4,7 @@ import React, { useContext } from "react";
import Link from "next/link";
import { Logo } from "@/components/Logo";
import { NEXT_PUBLIC_DO_NOT_USE_TOGGLE_OFF_DANSWER_POWERED } from "@/lib/constants";
import { HeaderTitle } from "@/components/header/Header";
import { HeaderTitle } from "@/components/header/HeaderTitle";
import { SettingsContext } from "@/components/settings/SettingsProvider";
import { BackIcon } from "@/components/icons/icons";

View File

@ -1,22 +1,17 @@
"use client";
import { User } from "@/lib/types";
import { TbLayoutSidebarLeftExpand } from "react-icons/tb";
import { UserDropdown } from "../UserDropdown";
import { FiShare2, FiSidebar } from "react-icons/fi";
import { FiShare2 } from "react-icons/fi";
import { SetStateAction, useContext, useEffect } from "react";
import { Logo } from "../Logo";
import { ChatIcon, NewChatIcon, PlusCircleIcon } from "../icons/icons";
import {
NEXT_PUBLIC_DO_NOT_USE_TOGGLE_OFF_DANSWER_POWERED,
NEXT_PUBLIC_NEW_CHAT_DIRECTS_TO_SAME_PERSONA,
} from "@/lib/constants";
import { NewChatIcon } from "../icons/icons";
import { NEXT_PUBLIC_NEW_CHAT_DIRECTS_TO_SAME_PERSONA } from "@/lib/constants";
import { ChatSession } from "@/app/chat/interfaces";
import { HeaderTitle } from "../header/Header";
import { Tooltip } from "../tooltip/Tooltip";
import KeyboardSymbol from "@/lib/browserUtilities";
import Link from "next/link";
import { SettingsContext } from "../settings/SettingsProvider";
import { pageType } from "@/app/chat/sessionSidebar/types";
import { useRouter } from "next/navigation";
import { ChatBanner } from "@/app/chat/ChatBanner";
import LogoType from "../header/LogoType";
export default function FunctionalHeader({
user,
@ -25,10 +20,12 @@ export default function FunctionalHeader({
setSharingModalVisible,
toggleSidebar,
reset = () => null,
sidebarToggled,
}: {
reset?: () => void;
page: pageType;
user: User | null;
sidebarToggled?: boolean;
currentChatSession?: ChatSession | null | undefined;
setSharingModalVisible?: (value: SetStateAction<boolean>) => void;
toggleSidebar: () => void;
@ -36,8 +33,6 @@ export default function FunctionalHeader({
const combinedSettings = useContext(SettingsContext);
const enterpriseSettings = combinedSettings?.enterpriseSettings;
const commandSymbol = KeyboardSymbol();
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.metaKey || event.ctrlKey) {
@ -74,43 +69,40 @@ export default function FunctionalHeader({
};
return (
<div className="pb-6 left-0 sticky top-0 z-20 w-full relative flex">
<div className="mt-2 mx-4 text-text-700 flex w-full">
<div className="absolute z-[100] my-auto flex items-center text-xl font-bold">
<button
onClick={() => toggleSidebar()}
className="pt-[2px] desktop:invisible mb-auto"
>
<FiSidebar size={20} />
</button>
<div className="invisible break-words inline-block w-fit ml-2 text-text-700 text-xl">
<div className="max-w-[200px]">
{enterpriseSettings && enterpriseSettings.application_name ? (
<div>
<HeaderTitle>
{enterpriseSettings.application_name}
</HeaderTitle>
{!NEXT_PUBLIC_DO_NOT_USE_TOGGLE_OFF_DANSWER_POWERED && (
<p className="text-xs text-subtle">Powered by Danswer</p>
)}
</div>
) : (
<HeaderTitle>Danswer</HeaderTitle>
)}
</div>
</div>
<div className="mt-2 mx-2.5 text-text-700 relative flex w-full">
<LogoType
page={page}
toggleSidebar={toggleSidebar}
handleNewChat={handleNewChat}
/>
{page == "chat" && (
<Tooltip delayDuration={1000} content="New Chat">
<button className="mobile:hidden my-auto" onClick={handleNewChat}>
<div className="cursor-pointer ml-2 flex-none text-text-700 hover:text-text-600 transition-colors duration-300">
<NewChatIcon size={20} />
</div>
</button>
</Tooltip>
)}
<div
style={{ transition: "width 0.30s ease-out" }}
className={`
mobile:hidden
flex-none
mx-auto
overflow-y-hidden
transition-all
duration-300
ease-in-out
h-full
${sidebarToggled ? "w-[250px]" : "w-[0px]"}
`}
/>
<div className="w-full mobile:-mx-20 desktop:px-4">
<ChatBanner />
</div>
<div className="ml-auto my-auto flex gap-x-2">
<div className="invisible">
<LogoType
page={page}
toggleSidebar={toggleSidebar}
handleNewChat={handleNewChat}
/>
</div>
<div className="absolute right-0 top-0 flex gap-x-2">
{setSharingModalVisible && (
<div
onClick={() => setSharingModalVisible(true)}
@ -133,7 +125,7 @@ export default function FunctionalHeader({
: "")
}
>
<div className="cursor-pointer ml-2 flex-none text-text-700 hover:text-text-600 transition-colors duration-300">
<div className="cursor-pointer mr-4 flex-none text-text-700 hover:text-text-600 transition-colors duration-300">
<NewChatIcon size={20} />
</div>
</Link>

View File

@ -1,78 +0,0 @@
"use client";
import { User } from "@/lib/types";
import Link from "next/link";
import React, { useContext } from "react";
import { HeaderWrapper } from "./HeaderWrapper";
import { SettingsContext } from "../settings/SettingsProvider";
import { UserDropdown } from "../UserDropdown";
import { Logo } from "../Logo";
import { NEXT_PUBLIC_DO_NOT_USE_TOGGLE_OFF_DANSWER_POWERED } from "@/lib/constants";
import { pageType } from "@/app/chat/sessionSidebar/types";
export function HeaderTitle({ children }: { children: JSX.Element | string }) {
const isString = typeof children === "string";
const textSize = isString && children.length > 10 ? "text-xl" : "text-2xl";
return (
<h1 className={`flex ${textSize} text-strong leading-none font-bold`}>
{children}
</h1>
);
}
interface HeaderProps {
user: User | null;
page?: pageType;
}
export function Header({ user, page }: HeaderProps) {
const combinedSettings = useContext(SettingsContext);
if (!combinedSettings) {
return null;
}
const settings = combinedSettings.settings;
const enterpriseSettings = combinedSettings.enterpriseSettings;
return (
<HeaderWrapper>
<div className="flex h-full">
<Link
className="py-3 flex flex-col"
href={
settings && settings.default_page === "chat" ? "/chat" : "/search"
}
>
<div className="max-w-[200px] flex my-auto">
<div className="mr-1 mb-auto">
<Logo />
</div>
<div className="my-auto">
{enterpriseSettings && enterpriseSettings.application_name ? (
<div>
<HeaderTitle>
{enterpriseSettings.application_name}
</HeaderTitle>
{!NEXT_PUBLIC_DO_NOT_USE_TOGGLE_OFF_DANSWER_POWERED && (
<p className="text-xs text-subtle">Powered by Danswer</p>
)}
</div>
) : (
<HeaderTitle>Danswer</HeaderTitle>
)}
</div>
</div>
</Link>
<div className="ml-auto h-full flex flex-col">
<div className="my-auto">
<UserDropdown user={user} page={page} />
</div>
</div>
</div>
</HeaderWrapper>
);
}
/*
*/

View File

@ -0,0 +1,14 @@
"use client";
import React from "react";
export function HeaderTitle({ children }: { children: JSX.Element | string }) {
const isString = typeof children === "string";
const textSize = isString && children.length > 10 ? "text-xl" : "text-2xl";
return (
<h1 className={`flex ${textSize} text-strong leading-none font-bold`}>
{children}
</h1>
);
}

View File

@ -0,0 +1,88 @@
"use effect";
import { useContext } from "react";
import { FiSidebar } from "react-icons/fi";
import { SettingsContext } from "../settings/SettingsProvider";
import { NEXT_PUBLIC_DO_NOT_USE_TOGGLE_OFF_DANSWER_POWERED } from "@/lib/constants";
import { LefToLineIcon, NewChatIcon, RightToLineIcon } from "../icons/icons";
import { Tooltip } from "../tooltip/Tooltip";
import { pageType } from "@/app/chat/sessionSidebar/types";
import { Logo } from "../Logo";
import { HeaderTitle } from "./HeaderTitle";
export default function LogoType({
toggleSidebar,
hideOnMobile,
handleNewChat,
page,
toggled,
showArrow,
}: {
hideOnMobile?: boolean;
toggleSidebar?: () => void;
handleNewChat?: () => void;
page: pageType;
toggled?: boolean;
showArrow?: boolean;
}) {
const combinedSettings = useContext(SettingsContext);
const enterpriseSettings = combinedSettings?.enterpriseSettings;
return (
<div
className={`${hideOnMobile && "mobile:hidden"} z-[100] mb-auto shrink-0 flex items-center text-xl font-bold`}
>
{toggleSidebar && page == "chat" ? (
<button
onClick={() => toggleSidebar()}
className="pt-[2px] ml-4 desktop:invisible mb-auto"
>
<FiSidebar size={20} />
</button>
) : (
<div className="mr-1 invisible mb-auto h-6 w-6">
<Logo height={24} width={24} />
</div>
)}
<div
className={` ${showArrow ? "desktop:invisible" : "invisible"} break-words inline-block w-fit ml-2 text-text-700 text-xl`}
>
<div className="max-w-[175px]">
{enterpriseSettings && enterpriseSettings.application_name ? (
<div>
<HeaderTitle>{enterpriseSettings.application_name}</HeaderTitle>
{!NEXT_PUBLIC_DO_NOT_USE_TOGGLE_OFF_DANSWER_POWERED && (
<p className="text-xs text-subtle">Powered by Danswer</p>
)}
</div>
) : (
<HeaderTitle>Danswer</HeaderTitle>
)}
</div>
</div>
{page == "chat" && !showArrow && (
<Tooltip delayDuration={1000} content="New Chat">
<button className="my-auto mobile:hidden" onClick={handleNewChat}>
<div className="cursor-pointer ml-2 flex-none text-text-700 hover:text-text-600 transition-colors duration-300">
<NewChatIcon size={20} />
</div>
</button>
</Tooltip>
)}
{showArrow && (
<Tooltip
delayDuration={0}
content={toggled ? `Unpin sidebar` : "Pin sidebar"}
>
<button className="mr-3 my-auto ml-auto" onClick={toggleSidebar}>
{!toggled && !combinedSettings?.isMobile ? (
<RightToLineIcon />
) : (
<LefToLineIcon />
)}
</button>
</Tooltip>
)}
</div>
);
}

View File

@ -514,6 +514,8 @@ export const SearchSection = ({
});
}
const chatBannerPresent = settings?.enterpriseSettings?.custom_header_content;
const { popup, setPopup } = usePopup();
return (
@ -553,6 +555,7 @@ export const SearchSection = ({
<div className="absolute left-0 w-full top-0">
<FunctionalHeader
sidebarToggled={toggledSidebar}
reset={() => setQuery("")}
toggleSidebar={toggleSidebar}
page="search"
@ -575,7 +578,9 @@ export const SearchSection = ({
/>
{
<div className="desktop:px-24 w-full pt-10 relative max-w-[2000px] xl:max-w-[1430px] mx-auto">
<div
className={`desktop:px-24 w-full ${chatBannerPresent && "mt-10"} pt-10 relative max-w-[2000px] xl:max-w-[1430px] mx-auto`}
>
<div className="absolute z-10 mobile:px-4 mobile:max-w-searchbar-max mobile:w-[90%] top-12 desktop:left-0 hidden 2xl:block mobile:left-1/2 mobile:transform mobile:-translate-x-1/2 desktop:w-52 3xl:w-64">
{!settings?.isMobile &&
(ccPairs.length > 0 || documentSets.length > 0) && (